From 86c796090d3319ed5dc3a0bf7f76712719896302 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Mon, 28 Feb 2022 15:52:30 -0700 Subject: [PATCH 001/427] Signature Verification with phpseclib3 --- activitypub.php | 2 + composer.json | 3 +- includes/class-signature.php | 133 +++++++++++++++++++++++++++++++++ includes/functions.php | 5 ++ includes/rest/class-inbox.php | 1 + includes/rest/class-server.php | 2 +- 6 files changed, 144 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 55d569a..1e69abc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -15,6 +15,8 @@ namespace Activitypub; +require __DIR__ . '/vendor/autoload.php'; + /** * Initialize plugin */ diff --git a/composer.json b/composer.json index 76d9b4c..0e70dc7 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "~1.0" + "composer/installers": "~1.0", + "phpseclib/phpseclib": "~3.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", diff --git a/includes/class-signature.php b/includes/class-signature.php index 5caf884..b89b87f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,6 +1,8 @@ + (https?:\/\/[\w\-\.]+[\w]+) + (:[\d]+)? + ([\w\-\.#\/@]+) + )", + (algorithm="(?P[\w\s-]+)",)? + (headers="(?P[\(\)\w\s-]+)",)? + signature="(?P[\w+\/]+={0,2})" + /x'; + + /** + * Allowed keys when splitting signature + * + * @var array + */ + private $allowedKeys = [ + 'keyId', + 'algorithm', // optional + 'headers', // optional + 'signature', + ]; + /** * @param int $user_id * @@ -109,6 +134,114 @@ class Signature { public static function verify_signature( $headers, $signature ) { + // https://github.com/landrok/activitypub/blob/master/src/ActivityPhp/Server/Http/HttpSignature.php + $header_data = $request->get_headers(); + $body = $request->get_body(); + if ( !$header_data['signature'][0] ) { + return false; + } + // Split it into its parts ( keyId, headers and signature ) + $signature_parts = self::splitSignature( $header_data['signature'][0] ); + if ( !count($signature_parts ) ) { + return false; + } + extract( $signature_parts ); + + // Fetch the public key linked from keyId + $actor = \strip_fragment_from_url( $keyId ); + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); + + if (! is_wp_error( $publicKeyPem ) ) { + $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); + $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); + if ( $digest_gen !== $header_data['digest'][0] ) { + return false; + } + // Create a comparison string from the plaintext headers we got + // in the same order as was given in the signature header, + $data_plain = self::getPlainText( + explode(' ', trim( $headers ) ), + $request + ); + + // Verify that string using the public key and the original + // signature. + $rsa = RSA::createKey() + ->loadPublicKey( $pkey['key']) + ->withHash('sha256'); + + $verified = $rsa->verify( $data_plain, \base64_decode( $signature ) ); + + if ( '1' === $verified ) { + return true; + } else { + return false; + } + } else { + $activity = json_decode($body); + if ( $activity->type === 'Delete' ) { + // TODO eventually process ld signatures + } + } + } + + /** + * Split HTTP signature into its parts (keyId, headers and signature) + */ + public static function splitSignature( $signature ) { + + $allowedKeys = [ + 'keyId', + 'algorithm', // optional + 'headers', // optional + 'signature', + ]; + + if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) { + \error_log('Signature pattern failed' . print_r( $signature, true ) ); + return []; + } + + // Headers are optional + if (!isset($matches['headers']) || $matches['headers'] == '') { + $matches['headers'] = 'date'; + } + + return array_filter($matches, function($key) use ($allowedKeys) { + return !is_int($key) && in_array($key, $allowedKeys); + }, ARRAY_FILTER_USE_KEY ); + } + + /** + * Get plain text that has been originally signed + * + * @param array $headers HTTP header keys + * @param \Symfony\Component\HttpFoundation\Request $request + */ + public static function getPlainText( $headers, $request ) { + + $url_params = $request->get_url_params(); + if ( isset( $url_params ) && isset( $url_params['user_id'] ) ) { + $url_params = ''; + } + + $strings = []; + $request_target = sprintf( + '%s %s%s', + strtolower($request->get_method()), + $request->get_route(), + $url_params + ); + + foreach ($headers as $value) { + if ( $value == '(request-target)' ) { + $strings[] = "$value: " . $request_target; + } else { + $strings[] = "$value: " . $request->get_header($value); + } + } + + return implode("\n", $strings); } public static function generate_digest( $body ) { diff --git a/includes/functions.php b/includes/functions.php index 3c33664..94bfaba 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -182,6 +182,11 @@ function get_inbox_by_actor( $actor ) { */ function get_publickey_by_actor( $actor, $key_id ) { $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); + + //Other Implementations may include an 'operations' query (Zap) + if ( isset( $metadata['publicKey']['id'] ) ) { + $metadata['publicKey']['id'] = strtok($metadata['publicKey']['id'], "?"); + } if ( \is_wp_error( $metadata ) ) { return $metadata; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1ffe451..8c246d7 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -82,6 +82,7 @@ class Inbox { $headers = $request->get_headers(); // verify signature + \Activitypub\Signature::verify_signature( $request ); //\Activitypub\Signature::verify_signature( $headers, $key ); return $served; diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index ac89dca..685b315 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -24,7 +24,7 @@ class Server extends \WP_REST_Server { // check for content-sub-types like 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' if ( \preg_match( '/application\/([a-zA-Z+_-]+\+)json/', $content_type['value'] ) ) { - $request->set_header( 'Content-Type', 'application/json' ); + // Signature Verification requires headers to be intact } // make request filterable From 99630a58bb8498ff0197456d860f9b9783d15ba7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Mon, 28 Feb 2022 19:32:26 -0700 Subject: [PATCH 002/427] fixes --- includes/class-signature.php | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index b89b87f..a1eddfa 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -20,18 +20,6 @@ class Signature { (headers="(?P[\(\)\w\s-]+)",)? signature="(?P[\w+\/]+={0,2})" /x'; - - /** - * Allowed keys when splitting signature - * - * @var array - */ - private $allowedKeys = [ - 'keyId', - 'algorithm', // optional - 'headers', // optional - 'signature', - ]; /** * @param int $user_id @@ -132,7 +120,7 @@ class Signature { } } - public static function verify_signature( $headers, $signature ) { + public static function verify_signature( $request ) { // https://github.com/landrok/activitypub/blob/master/src/ActivityPhp/Server/Http/HttpSignature.php $header_data = $request->get_headers(); @@ -140,7 +128,8 @@ class Signature { if ( !$header_data['signature'][0] ) { return false; } - // Split it into its parts ( keyId, headers and signature ) + + // Split it into its parts ( keyId, headers and signature ) $signature_parts = self::splitSignature( $header_data['signature'][0] ); if ( !count($signature_parts ) ) { return false; @@ -157,6 +146,7 @@ class Signature { if ( $digest_gen !== $header_data['digest'][0] ) { return false; } + // Create a comparison string from the plaintext headers we got // in the same order as was given in the signature header, $data_plain = self::getPlainText( @@ -177,12 +167,8 @@ class Signature { } else { return false; } - } else { - $activity = json_decode($body); - if ( $activity->type === 'Delete' ) { - // TODO eventually process ld signatures - } } + return true; } /** @@ -198,7 +184,6 @@ class Signature { ]; if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) { - \error_log('Signature pattern failed' . print_r( $signature, true ) ); return []; } From 1f6e1cf37ccd2e3030cc1c554db06055dafbcb96 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 19 Mar 2022 20:19:59 -0600 Subject: [PATCH 003/427] add openss_verify method and openssl_error_string --- includes/class-signature.php | 46 ++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a1eddfa..518baa4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -129,21 +129,21 @@ class Signature { return false; } - // Split it into its parts ( keyId, headers and signature ) + // Split signature into its parts $signature_parts = self::splitSignature( $header_data['signature'][0] ); - if ( !count($signature_parts ) ) { + if ( !count( $signature_parts ) ) { return false; } - extract( $signature_parts ); + extract( $signature_parts );// $keyId, $algorithm, $headers, $signature // Fetch the public key linked from keyId $actor = \strip_fragment_from_url( $keyId ); - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); if (! is_wp_error( $publicKeyPem ) ) { $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); - if ( $digest_gen !== $header_data['digest'][0] ) { + if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { return false; } @@ -154,21 +154,41 @@ class Signature { $request ); - // Verify that string using the public key and the original - // signature. + // 2 methods because neither works ¯\_(ツ)_/¯ + // phpseclib method $rsa = RSA::createKey() ->loadPublicKey( $pkey['key']) ->withHash('sha256'); - - $verified = $rsa->verify( $data_plain, \base64_decode( $signature ) ); - - if ( '1' === $verified ) { + $verified = $rsa->verify( $signing_headers, \base64_decode( $signature ) ); + if ( $verified > 0 ) { + \error_log( '$rsa->verify: //return true;' ); return true; } else { + while ( $ossl_error = openssl_error_string() ) { + \error_log( '$rsa->verify(): ' . $ossl_error ); + } + $activity = \json_decode( $body ); + \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); return false; - } + } + + // openssl method + $verified = \openssl_verify( $signing_headers, + \base64_decode( \normalize_whitespace( $signature ) ), + $pkey['key'], + \OPENSSL_ALGO_SHA256 + ); + if ( $verified > 0 ) { + \error_log( 'openssl_verify: //return true;' ); + return true; + } else { + while ( $ossl_error = openssl_error_string() ) { + \error_log( 'openssl_error_string(): ' . $ossl_error ); + } + return false; + } } - return true; + return false; } /** From 16ae895312053b96904c22a956b1dee11eed058c Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 20 Mar 2022 20:57:01 -0600 Subject: [PATCH 004/427] fixes --- includes/class-signature.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 518baa4..625649f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -140,8 +140,11 @@ class Signature { $actor = \strip_fragment_from_url( $keyId ); $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); - if (! is_wp_error( $publicKeyPem ) ) { + if ( !is_wp_error( $publicKeyPem ) ) { + // Probably overkill since we already have a seemingly weelformed PEM $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); + + // Verify Digest $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { return false; @@ -149,7 +152,7 @@ class Signature { // Create a comparison string from the plaintext headers we got // in the same order as was given in the signature header, - $data_plain = self::getPlainText( + $signing_headers = self::getPlainText( explode(' ', trim( $headers ) ), $request ); @@ -169,7 +172,7 @@ class Signature { } $activity = \json_decode( $body ); \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); - return false; + //return false; } // openssl method @@ -185,7 +188,7 @@ class Signature { while ( $ossl_error = openssl_error_string() ) { \error_log( 'openssl_error_string(): ' . $ossl_error ); } - return false; + //return false; } } return false; From 63993b20b949173f70bc0631fd085eef1334fae6 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 20 Mar 2022 21:12:53 -0600 Subject: [PATCH 005/427] fix const --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 625649f..fa373e8 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -10,7 +10,7 @@ use phpseclib3\Crypt\RSA; */ class Signature { - public const SIGNATURE_PATTERN = '/^ + const SIGNATURE_PATTERN = '/^ keyId="(?P (https?:\/\/[\w\-\.]+[\w]+) (:[\d]+)? From e015da7f8f258d193ce1950e14be3123a44aee5a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 01:42:15 +0100 Subject: [PATCH 006/427] optimize publishing --- includes/class-activity-dispatcher.php | 78 +++++++++----------------- includes/class-activitypub.php | 2 +- includes/functions.php | 5 +- includes/model/class-activity.php | 17 +++++- includes/model/class-post.php | 2 +- 5 files changed, 48 insertions(+), 56 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index f8abd38..09fca79 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -13,7 +13,10 @@ class Activity_Dispatcher { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_send_post_activity', array( '\Activitypub\Activity_Dispatcher', 'send_post_activity' ) ); + // legacy + \add_action( 'activitypub_send_post_activity', array( '\Activitypub\Activity_Dispatcher', 'send_create_activity' ) ); + + \add_action( 'activitypub_send_create_activity', array( '\Activitypub\Activity_Dispatcher', 'send_create_activity' ) ); \add_action( 'activitypub_send_update_activity', array( '\Activitypub\Activity_Dispatcher', 'send_update_activity' ) ); \add_action( 'activitypub_send_delete_activity', array( '\Activitypub\Activity_Dispatcher', 'send_delete_activity' ) ); } @@ -23,38 +26,8 @@ class Activity_Dispatcher { * * @param \Activitypub\Model\Post $activitypub_post */ - public static function send_post_activity( Model\Post $activitypub_post ) { - // get latest version of post - $user_id = $activitypub_post->get_post_author(); - - $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); - $activitypub_activity->from_post( $activitypub_post ); - - $inboxes = \Activitypub\get_follower_inboxes( $user_id ); - - $followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' ); - foreach ( $activitypub_activity->get_cc() as $cc ) { - if ( $cc === $followers_url ) { - continue; - } - $inbox = \Activitypub\get_inbox_by_actor( $cc ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $cc; - } - - foreach ( $inboxes as $inbox => $to ) { - $to = array_values( array_unique( $to ) ); - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); - - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } + public static function send_create_activity( Model\Post $activitypub_post ) { + self::send_activity( $activitypub_post, 'Create' ); } /** @@ -62,19 +35,8 @@ class Activity_Dispatcher { * * @param \Activitypub\Model\Post $activitypub_post */ - public static function send_update_activity( $activitypub_post ) { - // get latest version of post - $user_id = $activitypub_post->get_post_author(); - - $activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL ); - $activitypub_activity->from_post( $activitypub_post ); - - foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); // phpcs:ignore - - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } + public static function send_update_activity( Model\Post $activitypub_post ) { + self::send_activity( $activitypub_post, 'Update' ); } /** @@ -82,16 +44,30 @@ class Activity_Dispatcher { * * @param \Activitypub\Model\Post $activitypub_post */ - public static function send_delete_activity( $activitypub_post ) { + public static function send_delete_activity( Model\Post $activitypub_post ) { + self::send_activity( $activitypub_post, 'Delete' ); + } + + /** + * Undocumented function + * + * @param [type] $activitypub_post + * @param [type] $activity_type + * @return void + */ + public function send_activity( Model\Post $activitypub_post, $activity_type ) { // get latest version of post $user_id = $activitypub_post->get_post_author(); - $activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL ); + $activitypub_activity = new \Activitypub\Model\Activity( $activity_type, \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post ); - foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { - $activitypub_activity->set_to( $to ); - $activity = $activitypub_activity->to_json(); // phpcs:ignore + $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); + + foreach ( $inboxes as $inbox => $cc ) { + $cc = array_values( array_unique( $cc ) ); + $activitypub_activity->add_cc( $cc ); + $activity = $activitypub_activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index c04aedc..4a60915 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -117,7 +117,7 @@ class Activitypub { $activitypub_post = new \Activitypub\Model\Post( $post ); if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( \time(), 'activitypub_send_post_activity', array( $activitypub_post ) ); + \wp_schedule_single_event( \time(), 'activitypub_send_create_activity', array( $activitypub_post ) ); } elseif ( 'publish' === $new_status ) { \wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) ); } elseif ( 'trash' === $new_status ) { diff --git a/includes/functions.php b/includes/functions.php index 8f2e5d9..77508c5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,8 +219,11 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } -function get_follower_inboxes( $user_id ) { +function get_follower_inboxes( $user_id, $cc = array() ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); + $followers = array_merge( $followers, $cc ); + $followers = array_unique( $followers ); + $inboxes = array(); foreach ( $followers as $follower ) { diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index f865d6d..c95e6ff 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -43,6 +43,18 @@ class Activity { if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { $this->$var = $params[0]; } + + if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { + if ( ! is_array( $this->$var ) ) { + $this->$var = $params[0]; + } + + if ( is_array( $params[0] ) ) { + $this->$var = array_merge( $this->$var, $params[0] ); + } else { + array_push( $this->$var, $params[0] ); + } + } } public function from_post( Post $post ) { @@ -51,7 +63,8 @@ class Activity { if ( isset( $object['published'] ) ) { $this->published = $object['published']; } - $this->cc = array( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) ); + + $this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) ); if ( isset( $this->object['attributedTo'] ) ) { $this->actor = $this->object['attributedTo']; @@ -59,7 +72,7 @@ class Activity { foreach ( $post->get_tags() as $tag ) { if ( 'Mention' === $tag['type'] ) { - $this->cc[] = $tag['href']; + $this->add_cc( $tag['href'] ); } } diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 543a890..5d2ac48 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -386,7 +386,7 @@ class Post { $filtered_content = \apply_filters( 'activitypub_the_content', $content, $post ); $decoded_content = \html_entity_decode( $filtered_content, \ENT_QUOTES, 'UTF-8' ); - $content = \trim( \preg_replace( '/[\n\r]/', '', $content ) ); + $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); $this->content = $content; From 472ee27849c561b241caac44cb04bdb827768abf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 01:47:12 +0100 Subject: [PATCH 007/427] fix unit tests --- tests/test-class-activitypub-activity-dispatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 6693ba1..ec62b2d 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -28,7 +28,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post ); + \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -67,7 +67,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post ); + \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); From 3c84be1691b7388e57d942911c0973671a0309c4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 01:50:20 +0100 Subject: [PATCH 008/427] fix unit tests --- includes/class-activity-dispatcher.php | 2 +- tests/test-class-activitypub-activity.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 09fca79..221e2b0 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -55,7 +55,7 @@ class Activity_Dispatcher { * @param [type] $activity_type * @return void */ - public function send_activity( Model\Post $activitypub_post, $activity_type ) { + public static function send_activity( Model\Post $activitypub_post, $activity_type ) { // get latest version of post $user_id = $activitypub_post->get_post_author(); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 254d860..74d0727 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_cc() ); + $this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); From e52181fd3733b6b9b126f5ce046bbe10207ea312 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 02:04:06 +0100 Subject: [PATCH 009/427] fix tests --- includes/model/class-activity.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index c95e6ff..220541e 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -15,7 +15,7 @@ class Activity { private $type = 'Create'; private $actor = ''; private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - private $cc = array( 'https://www.w3.org/ns/activitystreams#Public' ); + private $cc = array(); private $object = null; const TYPE_SIMPLE = 'simple'; @@ -54,6 +54,8 @@ class Activity { } else { array_push( $this->$var, $params[0] ); } + + $this->$var = array_unique( $this->$var ); } } From 365d5dd4997c22d7e6c2ef14a8e68e5a0bd36f7d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 02:35:57 +0100 Subject: [PATCH 010/427] fix outbox --- includes/rest/class-outbox.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 7eec5ac..677e2ff 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -102,8 +102,8 @@ class Outbox { foreach ( $posts as $post ) { $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_NONE ); - $activitypub_activity->from_post( $activitypub_post->to_array() ); + $activitypub_activity = new \Activitypub\Model\Activity( 'Create', false ); + $activitypub_activity->from_post( $activitypub_post ); $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore } } From de32cb7b7320bfa6fac5603ea7978059c1e83579 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 02:36:29 +0100 Subject: [PATCH 011/427] add changes also to the object --- includes/class-activity-dispatcher.php | 2 +- includes/model/class-activity.php | 32 ++++++++++++++++------- includes/model/class-post.php | 22 ++++++++++++++-- includes/rest/class-inbox.php | 2 +- tests/test-class-activitypub-activity.php | 2 +- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 221e2b0..6df83e7 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -59,7 +59,7 @@ class Activity_Dispatcher { // get latest version of post $user_id = $activitypub_post->get_post_author(); - $activitypub_activity = new \Activitypub\Model\Activity( $activity_type, \Activitypub\Model\Activity::TYPE_FULL ); + $activitypub_activity = new \Activitypub\Model\Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 220541e..34f6146 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -9,7 +9,27 @@ namespace Activitypub\Model; * @see https://www.w3.org/TR/activitypub/ */ class Activity { - private $context = array( 'https://www.w3.org/ns/activitystreams' ); + private $context = array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + array( + 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', + 'PropertyValue' => 'schema:PropertyValue', + 'schema' => 'http://schema.org#', + 'pt' => 'https://joinpeertube.org/ns#', + 'toot' => 'http://joinmastodon.org/ns#', + 'value' => 'schema:value', + 'Hashtag' => 'as:Hashtag', + 'featured' => array( + '@id' => 'toot:featured', + '@type' => '@id', + ), + 'featuredTags' => array( + '@id' => 'toot:featuredTags', + '@type' => '@id', + ), + ), + ); private $published = ''; private $id = ''; private $type = 'Create'; @@ -18,15 +38,9 @@ class Activity { private $cc = array(); private $object = null; - const TYPE_SIMPLE = 'simple'; - const TYPE_FULL = 'full'; - const TYPE_NONE = 'none'; - - public function __construct( $type = 'Create', $context = self::TYPE_SIMPLE ) { - if ( 'none' === $context ) { + public function __construct( $type = 'Create', $context = true ) { + if ( true !== $context ) { $this->context = null; - } elseif ( 'full' === $context ) { - $this->context = \Activitypub\get_context(); } $this->type = \ucfirst( $type ); diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 5d2ac48..90f19ef 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -87,6 +87,9 @@ class Post { ), ); + private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); + private $cc = array(); + /** * Constructor * @@ -94,6 +97,7 @@ class Post { */ public function __construct( $post ) { $this->post = \get_post( $post ); + $this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $this->get_post_author() ) . '/followers' ) ); } /** @@ -117,6 +121,20 @@ class Post { if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { $this->$var = $params[0]; } + + if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { + if ( ! is_array( $this->$var ) ) { + $this->$var = $params[0]; + } + + if ( is_array( $params[0] ) ) { + $this->$var = array_merge( $this->$var, $params[0] ); + } else { + array_push( $this->$var, $params[0] ); + } + + $this->$var = array_unique( $this->$var ); + } } /** @@ -138,8 +156,8 @@ class Post { 'contentMap' => array( \strstr( \get_locale(), '_', true ) => $this->get_content(), ), - 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), - 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), + 'to' => $this->get_to(), + 'cc' => $this->get_cc(), 'attachment' => $this->get_attachments(), 'tag' => $this->get_tags(), ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 5c21f43..1eab8a5 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -356,7 +356,7 @@ class Inbox { $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); // send "Accept" activity - $activity = new \Activitypub\Model\Activity( 'Accept', \Activitypub\Model\Activity::TYPE_SIMPLE ); + $activity = new \Activitypub\Model\Activity( 'Accept' ); $activity->set_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); $activity->set_to( $object['actor'] ); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 74d0727..7fe6551 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -19,7 +19,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); + $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); $this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_to() ); From 73ae47e37729047088bb34dbff618ca76468b3c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 2 Feb 2023 07:24:27 +0100 Subject: [PATCH 012/427] PHPDoc --- includes/model/class-post.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 90f19ef..0929277 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -87,7 +87,22 @@ class Post { ), ); + /** + * List of audience + * + * Also used for visibility + * + * @var array + */ private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); + + /** + * List of audience + * + * Also used for visibility + * + * @var array + */ private $cc = array(); /** From abef17b9ad5d8e346aca92008e3b2b13aaa4ebf4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 11 Mar 2023 10:58:05 +0100 Subject: [PATCH 013/427] add Automattic as Co-Author --- activitypub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index b72901b..7320f0c 100644 --- a/activitypub.php +++ b/activitypub.php @@ -4,8 +4,8 @@ * 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: 0.17.0 - * Author: Matthias Pfefferle - * Author URI: https://notiz.blog/ + * Author: Matthias Pfefferle & Automattic + * Author URI: https://automattic.com/ * License: MIT * License URI: http://opensource.org/licenses/MIT * Requires PHP: 5.6 From 5200eb24636238657ce6177ebdd09b637577e849 Mon Sep 17 00:00:00 2001 From: Edward Ficklin Date: Tue, 14 Mar 2023 13:34:50 -0400 Subject: [PATCH 014/427] define const for fedi bio meta key --- activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/activitypub.php b/activitypub.php index b72901b..1a23d8d 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

From becef59452405424db02af0e56579e9fce39f0c5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sat, 18 Mar 2023 21:59:09 +0100 Subject: [PATCH 021/427] improve readme thanks a lot @cavalierlife --- README.md | 62 ++++++++++++++++++++++++++++++++++++++---------------- readme.txt | 62 ++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 88 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 969f562..7b394c1 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,11 @@ The ActivityPub protocol is a decentralized social networking protocol based upo ## Description ## -This is **BETA** software, see the FAQ to see the current feature set or rather what is still planned. +This is BETA software, see the FAQ to see the current feature set or rather what is still planned. -The plugin implements the ActivityPub protocol for your blog. Your readers will be able to follow your blogposts on Mastodon and other federated platforms that support ActivityPub. +The plugin implements the ActivityPub protocol for your blog, which means that your readers will be able to follow your blog posts on Mastodon and other federated platforms that support ActivityPub. In addition, replies to your posts on Mastodon and related platforms will automatically become comments on your blog post. -The plugin works with the following federated platforms: +The plugin works with the following tested federated platforms, but there may be more that it works with as well: * [Mastodon](https://joinmastodon.org/) * [Pleroma](https://pleroma.social/) @@ -26,8 +26,44 @@ The plugin works with the following federated platforms: * [SocialHome](https://socialhome.network/) * [Misskey](https://join.misskey.page/) +Here’s what that means and what you can expect. + +Once the ActivityPub plugin is installed, each author’s page on your WordPress blog will become its own federated instance. In other words, if you have two authors, Jane and Bob, on your website, `example.com`, then your authors would have their own author pages at `example.com/author/jane` and `example.com/author/bob`. Each of those author pages would now be available to Mastodon users (and all other federated platform users) as a profile that can be followed. Let’s break that down further. Let’s say you have a friend on Mastodon who tells you to follow them and they give you their profile name `@janelivesheresomeofthetime@mastodon.social`. You search for her name, see her profile, and click the follow button, right? From then on, everything Jane posts on her profile shows up in your Home feed. Okay, similarly, now that Jane has installed the ActivityPub plugin on her `example.com` site, her friends can also follow her on Mastodon by searching for `@jane@example.com` and clicking the Follow button on that profile. + +From now on, every blog post Jane publishes on example.com will show up on your Home feed because you follow her `@jane@example.com` profile. +Of course, if no one follows your author instance, then no one will ever see the posts - including you! So the easiest way to even know if the plugin is working is to follow your new profile yourself. If you already have a Mastodon profile, just follow your new one from there. + +Some things to note: + + 1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. + 2. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. + +So what’s the process? + + 1. Install the ActivityPub plugin. + 2. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. + 3. Make sure your blog’s author profile page is active. + 4. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. + 5. On your blog, publish a new post. + 6. From Mastodon, check to see if the new post appears in your Home feed. + +Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please don’t assume that just because you didn’t see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and you’ll know everything is working as expected. + ## Frequently Asked Questions ## +### tl;dr ### + +This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog's author pages can be followed by users on these platforms, allowing them to receive your new posts in their feeds. + +Here's how it works: + + 1. Install the plugin and adjust settings as needed. + 2. Ensure your blog's author profile page is active. + 3. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). + 4. Publish a new post on your blog and check if it appears in your Mastodon feed. + +Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time. + ### What is the status of this plugin? ### Implemented: @@ -50,16 +86,6 @@ To implement: *ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/). -### What are the differences between this plugin and Pterotype? ### - -**Compatibility** - -*ActivityPub for WordPress* is compatible with OStatus and IndieWeb plugin suites. *Pterotype* is incompatible with the standalone [WebFinger plugin](https://wordpress.org/plugins/webfinger/), so it can't be run together with OStatus. - -**Custom tables** - -*Pterotype* creates/uses a bunch of custom tables, *ActivityPub for WordPress* only uses the native tables and adds as little meta data as possible. - ### What if you are running your blog in a subdirectory? ### In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides. @@ -356,12 +382,12 @@ Follow the normal instructions for [installing WordPress plugins](https://wordpr To add a WordPress Plugin using the [built-in plugin installer](https://codex.wordpress.org/Administration_Screens#Add_New_Plugins): 1. Go to [Plugins](https://codex.wordpress.org/Administration_Screens#Plugins) > [Add New](https://codex.wordpress.org/Plugins_Add_New_Screen). -1. Type "`activitypub`" into the **Search Plugins** box. -1. Find the WordPress Plugin you wish to install. +2. Type "`activitypub`" into the **Search Plugins** box. +3. Find the WordPress Plugin you wish to install. 1. Click **Details** for more information about the Plugin and instructions you may wish to print or save to help setup the Plugin. - 1. Click **Install Now** to install the WordPress Plugin. -1. The resulting installation screen will list the installation as successful or note any problems during the install. -1. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. + 2. Click **Install Now** to install the WordPress Plugin. +4. The resulting installation screen will list the installation as successful or note any problems during the install. +5. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. ### Manual Plugin Installation ### diff --git a/readme.txt b/readme.txt index 63e1f6a..d47f644 100644 --- a/readme.txt +++ b/readme.txt @@ -12,11 +12,11 @@ The ActivityPub protocol is a decentralized social networking protocol based upo == Description == -This is **BETA** software, see the FAQ to see the current feature set or rather what is still planned. +This is BETA software, see the FAQ to see the current feature set or rather what is still planned. -The plugin implements the ActivityPub protocol for your blog. Your readers will be able to follow your blogposts on Mastodon and other federated platforms that support ActivityPub. +The plugin implements the ActivityPub protocol for your blog, which means that your readers will be able to follow your blog posts on Mastodon and other federated platforms that support ActivityPub. In addition, replies to your posts on Mastodon and related platforms will automatically become comments on your blog post. -The plugin works with the following federated platforms: +The plugin works with the following tested federated platforms, but there may be more that it works with as well: * [Mastodon](https://joinmastodon.org/) * [Pleroma](https://pleroma.social/) @@ -26,8 +26,44 @@ The plugin works with the following federated platforms: * [SocialHome](https://socialhome.network/) * [Misskey](https://join.misskey.page/) +Here’s what that means and what you can expect. + +Once the ActivityPub plugin is installed, each author’s page on your WordPress blog will become its own federated instance. In other words, if you have two authors, Jane and Bob, on your website, `example.com`, then your authors would have their own author pages at `example.com/author/jane` and `example.com/author/bob`. Each of those author pages would now be available to Mastodon users (and all other federated platform users) as a profile that can be followed. Let’s break that down further. Let’s say you have a friend on Mastodon who tells you to follow them and they give you their profile name `@janelivesheresomeofthetime@mastodon.social`. You search for her name, see her profile, and click the follow button, right? From then on, everything Jane posts on her profile shows up in your Home feed. Okay, similarly, now that Jane has installed the ActivityPub plugin on her `example.com` site, her friends can also follow her on Mastodon by searching for `@jane@example.com` and clicking the Follow button on that profile. + +From now on, every blog post Jane publishes on example.com will show up on your Home feed because you follow her `@jane@example.com` profile. +Of course, if no one follows your author instance, then no one will ever see the posts - including you! So the easiest way to even know if the plugin is working is to follow your new profile yourself. If you already have a Mastodon profile, just follow your new one from there. + +Some things to note: + + 1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. + 2. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. + +So what’s the process? + + 1. Install the ActivityPub plugin. + 2. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. + 3. Make sure your blog’s author profile page is active. + 4. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. + 5. On your blog, publish a new post. + 6. From Mastodon, check to see if the new post appears in your Home feed. + +Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please don’t assume that just because you didn’t see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and you’ll know everything is working as expected. + == Frequently Asked Questions == += tl;dr = + +This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog's author pages can be followed by users on these platforms, allowing them to receive your new posts in their feeds. + +Here's how it works: + + 1. Install the plugin and adjust settings as needed. + 2. Ensure your blog's author profile page is active. + 3. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). + 4. Publish a new post on your blog and check if it appears in your Mastodon feed. + +Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time. + = What is the status of this plugin? = Implemented: @@ -50,16 +86,6 @@ To implement: *ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/). -= What are the differences between this plugin and Pterotype? = - -**Compatibility** - -*ActivityPub for WordPress* is compatible with OStatus and IndieWeb plugin suites. *Pterotype* is incompatible with the standalone [WebFinger plugin](https://wordpress.org/plugins/webfinger/), so it can't be run together with OStatus. - -**Custom tables** - -*Pterotype* creates/uses a bunch of custom tables, *ActivityPub for WordPress* only uses the native tables and adds as little meta data as possible. - = What if you are running your blog in a subdirectory? = In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides. @@ -356,12 +382,12 @@ Follow the normal instructions for [installing WordPress plugins](https://wordpr To add a WordPress Plugin using the [built-in plugin installer](https://codex.wordpress.org/Administration_Screens#Add_New_Plugins): 1. Go to [Plugins](https://codex.wordpress.org/Administration_Screens#Plugins) > [Add New](https://codex.wordpress.org/Plugins_Add_New_Screen). -1. Type "`activitypub`" into the **Search Plugins** box. -1. Find the WordPress Plugin you wish to install. +2. Type "`activitypub`" into the **Search Plugins** box. +3. Find the WordPress Plugin you wish to install. 1. Click **Details** for more information about the Plugin and instructions you may wish to print or save to help setup the Plugin. - 1. Click **Install Now** to install the WordPress Plugin. -1. The resulting installation screen will list the installation as successful or note any problems during the install. -1. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. + 2. Click **Install Now** to install the WordPress Plugin. +4. The resulting installation screen will list the installation as successful or note any problems during the install. +5. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. = Manual Plugin Installation = From 70fe654c95624ec6fd0c50ad1c63116ac4597f67 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 19 Mar 2023 08:42:33 +0100 Subject: [PATCH 022/427] fix ordered lists --- README.md | 34 +++++++++++++++++----------------- readme.txt | 34 +++++++++++++++++----------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 7b394c1..d1b4461 100644 --- a/README.md +++ b/README.md @@ -35,17 +35,17 @@ Of course, if no one follows your author instance, then no one will ever see the Some things to note: - 1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. - 2. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. +1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. +1. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. So what’s the process? - 1. Install the ActivityPub plugin. - 2. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. - 3. Make sure your blog’s author profile page is active. - 4. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. - 5. On your blog, publish a new post. - 6. From Mastodon, check to see if the new post appears in your Home feed. +1. Install the ActivityPub plugin. +1. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. +1. Make sure your blog’s author profile page is active. +1. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. +1. On your blog, publish a new post. +1. From Mastodon, check to see if the new post appears in your Home feed. Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please don’t assume that just because you didn’t see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and you’ll know everything is working as expected. @@ -57,10 +57,10 @@ This plugin connects your WordPress blog to popular social platforms like Mastod Here's how it works: - 1. Install the plugin and adjust settings as needed. - 2. Ensure your blog's author profile page is active. - 3. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). - 4. Publish a new post on your blog and check if it appears in your Mastodon feed. +1. Install the plugin and adjust settings as needed. +1. Ensure your blog's author profile page is active. +1. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). +1. Publish a new post on your blog and check if it appears in your Mastodon feed. Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time. @@ -382,12 +382,12 @@ Follow the normal instructions for [installing WordPress plugins](https://wordpr To add a WordPress Plugin using the [built-in plugin installer](https://codex.wordpress.org/Administration_Screens#Add_New_Plugins): 1. Go to [Plugins](https://codex.wordpress.org/Administration_Screens#Plugins) > [Add New](https://codex.wordpress.org/Plugins_Add_New_Screen). -2. Type "`activitypub`" into the **Search Plugins** box. -3. Find the WordPress Plugin you wish to install. +1. Type "`activitypub`" into the **Search Plugins** box. +1. Find the WordPress Plugin you wish to install. 1. Click **Details** for more information about the Plugin and instructions you may wish to print or save to help setup the Plugin. - 2. Click **Install Now** to install the WordPress Plugin. -4. The resulting installation screen will list the installation as successful or note any problems during the install. -5. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. + 1. Click **Install Now** to install the WordPress Plugin. +1. The resulting installation screen will list the installation as successful or note any problems during the install. +1. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. ### Manual Plugin Installation ### diff --git a/readme.txt b/readme.txt index d47f644..9c7661d 100644 --- a/readme.txt +++ b/readme.txt @@ -35,17 +35,17 @@ Of course, if no one follows your author instance, then no one will ever see the Some things to note: - 1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. - 2. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. +1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will enable the page to be live and ActivityPub will now work, but the live page won’t cause any duplicate content issues with search engines. +1. Once ActivityPub is installed, only new posts going forward will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site, will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on. So what’s the process? - 1. Install the ActivityPub plugin. - 2. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. - 3. Make sure your blog’s author profile page is active. - 4. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. - 5. On your blog, publish a new post. - 6. From Mastodon, check to see if the new post appears in your Home feed. +1. Install the ActivityPub plugin. +1. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready. +1. Make sure your blog’s author profile page is active. +1. Go to Mastodon or any other federated platform, search for your author’s new federated profile, and follow it. Your new profile will be in the form of @yourauthorname@yourwebsite.com, so that is what you’ll search for. +1. On your blog, publish a new post. +1. From Mastodon, check to see if the new post appears in your Home feed. Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please don’t assume that just because you didn’t see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and you’ll know everything is working as expected. @@ -57,10 +57,10 @@ This plugin connects your WordPress blog to popular social platforms like Mastod Here's how it works: - 1. Install the plugin and adjust settings as needed. - 2. Ensure your blog's author profile page is active. - 3. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). - 4. Publish a new post on your blog and check if it appears in your Mastodon feed. +1. Install the plugin and adjust settings as needed. +1. Ensure your blog's author profile page is active. +1. On Mastodon or other supported platforms, search for and follow your author's new profile (e.g., `@yourauthorname@yourwebsite.com`). +1. Publish a new post on your blog and check if it appears in your Mastodon feed. Please note that it may take up to 15 minutes for a new post to appear in your feed, as messages are sent on a delay to avoid overwhelming your followers. Be patient and give it some time. @@ -382,12 +382,12 @@ Follow the normal instructions for [installing WordPress plugins](https://wordpr To add a WordPress Plugin using the [built-in plugin installer](https://codex.wordpress.org/Administration_Screens#Add_New_Plugins): 1. Go to [Plugins](https://codex.wordpress.org/Administration_Screens#Plugins) > [Add New](https://codex.wordpress.org/Plugins_Add_New_Screen). -2. Type "`activitypub`" into the **Search Plugins** box. -3. Find the WordPress Plugin you wish to install. +1. Type "`activitypub`" into the **Search Plugins** box. +1. Find the WordPress Plugin you wish to install. 1. Click **Details** for more information about the Plugin and instructions you may wish to print or save to help setup the Plugin. - 2. Click **Install Now** to install the WordPress Plugin. -4. The resulting installation screen will list the installation as successful or note any problems during the install. -5. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. + 1. Click **Install Now** to install the WordPress Plugin. +1. The resulting installation screen will list the installation as successful or note any problems during the install. +1. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions. = Manual Plugin Installation = From 7d11d3e2089d49c4e1626e4a73ca192aa5881eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Wrede?= Date: Thu, 23 Mar 2023 08:35:26 +0100 Subject: [PATCH 023/427] Fix documentation and typos. --- includes/class-health-check.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 26bc808..248d260 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -35,7 +35,7 @@ class Health_Check { /** * Author URL tests * - * @return void + * @return array */ public static function test_author_url() { $result = array( @@ -73,7 +73,7 @@ class Health_Check { /** * WebFinger tests * - * @return void + * @return array */ public static function test_webfinger() { $result = array( @@ -85,7 +85,7 @@ class Health_Check { ), 'description' => \sprintf( '

%s

', - \__( 'Your WebFinger endpoint is accessible and returns the correct informations.', 'activitypub' ) + \__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' ) ), 'actions' => '', 'test' => 'test_webfinger', @@ -109,9 +109,9 @@ class Health_Check { } /** - * Check if `author_posts_url` is accessible and that requerst returns correct JSON + * Check if `author_posts_url` is accessible and that request returns correct JSON * - * @return boolean|WP_Error + * @return boolean|\WP_Error */ public static function is_author_url_accessible() { $user = \wp_get_current_user(); @@ -194,9 +194,9 @@ class Health_Check { } /** - * Check if WebFinger endoint is accessible and profile requerst returns correct JSON + * Check if WebFinger endpoint is accessible and profile request returns correct JSON * - * @return boolean|WP_Error + * @return boolean|\WP_Error */ public static function is_webfinger_endpoint_accessible() { $user = \wp_get_current_user(); @@ -272,7 +272,7 @@ class Health_Check { * Static function for generating site debug data when required. * * @param array $info The debug information to be added to the core information page. - * @return array The filtered informations + * @return array The filtered information */ public static function debug_information( $info ) { $info['activitypub'] = array( From 0c7cec3eba21dfc139dd80d2b214f3247df4a3c8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 1 Apr 2023 10:17:56 -0600 Subject: [PATCH 024/427] Fix signature parse verification --- includes/class-signature.php | 286 +++++++++++++++++++--------------- includes/rest/class-inbox.php | 11 +- 2 files changed, 172 insertions(+), 125 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 2f136b3..a4c45f4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,7 +1,8 @@ - (https?:\/\/[\w\-\.]+[\w]+) - (:[\d]+)? - ([\w\-\.#\/@]+) - )", - (algorithm="(?P[\w\s-]+)",)? - (headers="(?P[\(\)\w\s-]+)",)? - signature="(?P[\w+\/]+={0,2})" - /x'; + /** + * How much leeway to provide on the date header in seconds. + * Not everybody uses NTP. + */ + const MAX_TIME_OFFSET = 10800; + + const DEFAULT_SIGNING_ALGORITHM = 'sha256'; /** * @param int $user_id @@ -120,136 +118,176 @@ class Signature { } } - public static function verify_signature( $request ) { + public static function verify_signature( $request = null ) { + $headers = $request->get_headers(); + $headers["(request-target)"][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - // https://github.com/landrok/activitypub/blob/master/src/ActivityPhp/Server/Http/HttpSignature.php - $header_data = $request->get_headers(); - $body = $request->get_body(); - if ( !$header_data['signature'][0] ) { - return false; - } - - // Split signature into its parts - $signature_parts = self::splitSignature( $header_data['signature'][0] ); - if ( !count( $signature_parts ) ) { - return false; - } - extract( $signature_parts );// $keyId, $algorithm, $headers, $signature + if ( !$headers ) { + $headers = self::default_server_headers(); + } + if ( array_key_exists( 'signature', $headers ) ) { + $signature_block = self::parse_signature_header( $headers['signature'] ); + } elseif ( array_key_exists( 'authorization', $headers ) ) { + $signature_block = self::parse_signature_header( $headers['authorization'] ); + } - // Fetch the public key linked from keyId - $actor = \strip_fragment_from_url( $keyId ); - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); - - if ( !is_wp_error( $publicKeyPem ) ) { - // Probably overkill since we already have a seemingly weelformed PEM - $pkey = \openssl_pkey_get_details( \openssl_pkey_get_public( $publicKeyPem ) ); + if ( !$signature_block ) { + return false; + } - // Verify Digest - $digest_gen = 'SHA-256=' . \base64_encode( \hash( 'sha256', $body, true ) ); - if ( ! isset( $header_data['digest'][0] ) || ( $digest_gen !== $header_data['digest'][0] ) ) { + $signed_headers = $signature_block['headers']; + if ( ! $signed_headers ) { + $signed_headers = ['date']; + } + + $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); + if ( ! $signed_data ) { + return false; + } + + $algorithm = self::get_signature_algorithm( $signature_block ); + if ( ! $algorithm ) { + return false; + } + + if ( in_array( 'digest', $signed_headers ) && isset( $body ) ) { + $digest = explode( '=', $headers['digest'], 2 ); + if ( $digest[0] === 'SHA-256' ) { + $hashalg = 'sha256'; + } + if ( $digest[0] === 'SHA-512' ) { + $hashalg = 'sha512'; + } + + // TODO Test + if ( base64_encode( hash( $hashalg, $body, true ) ) !== $digest[1] ) { return false; } + } - // Create a comparison string from the plaintext headers we got - // in the same order as was given in the signature header, - $signing_headers = self::getPlainText( - explode(' ', trim( $headers ) ), - $request - ); + $public_key = $key?? self::get_key( $signature_block['keyId'] ); - // 2 methods because neither works ¯\_(ツ)_/¯ - // phpseclib method - $rsa = RSA::createKey() - ->loadPublicKey( $pkey['key']) - ->withHash('sha256'); - $verified = $rsa->verify( $signing_headers, \base64_decode( $signature ) ); - if ( $verified > 0 ) { - \error_log( '$rsa->verify: //return true;' ); - return true; - } else { - while ( $ossl_error = openssl_error_string() ) { - \error_log( '$rsa->verify(): ' . $ossl_error ); - } - $activity = \json_decode( $body ); - \error_log( 'activity->type: ' . print_r( $activity->type, true ) ); - //return false; - } - - // openssl method - $verified = \openssl_verify( $signing_headers, - \base64_decode( \normalize_whitespace( $signature ) ), - $pkey['key'], - \OPENSSL_ALGO_SHA256 - ); - if ( $verified > 0 ) { - \error_log( 'openssl_verify: //return true;' ); - return true; - } else { - while ( $ossl_error = openssl_error_string() ) { - \error_log( 'openssl_error_string(): ' . $ossl_error ); - } - //return false; + return \openssl_verify( $signed_data,$signature_block['signature'], $public_key, $algorithm ) > 0; + + } + + public static function default_server_headers() { + $headers = array( + '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], + 'content-type' => $_SERVER['CONTENT_TYPE'], + 'content-length' => $_SERVER['CONTENT_LENGTH'], + ); + foreach ( $_SERVER as $k => $v ) { + if ( strpos( $k, 'HTTP_' ) === 0 ) { + $field = str_replace( '_', '-', strtolower( substr( $k, 5 ) ) ); + $headers[$field] = $v; } } + return $headers; + } + + public static function get_signature_algorithm( $signature_block ) { + switch ( $signature_block['algorithm'] ) { + case 'rsa-sha256': + return 'sha256'; + case 'rsa-sha-512': + return 'sha512'; + case 'hs2019': + return self::DEFAULT_SIGNING_ALGORITHM; + } return false; } - /** - * Split HTTP signature into its parts (keyId, headers and signature) - */ - public static function splitSignature( $signature ) { - - $allowedKeys = [ - 'keyId', - 'algorithm', // optional - 'headers', // optional - 'signature', - ]; + public static function parse_signature_header( $header ) { + $ret = []; + $matches = []; + $h_string = implode( ',', (array) $header[0] ); - if (!preg_match(self::SIGNATURE_PATTERN, $signature, $matches)) { - return []; - } - - // Headers are optional - if (!isset($matches['headers']) || $matches['headers'] == '') { - $matches['headers'] = 'date'; - } - - return array_filter($matches, function($key) use ($allowedKeys) { - return !is_int($key) && in_array($key, $allowedKeys); - }, ARRAY_FILTER_USE_KEY ); - } - - /** - * Get plain text that has been originally signed - * - * @param array $headers HTTP header keys - * @param \Symfony\Component\HttpFoundation\Request $request - */ - public static function getPlainText( $headers, $request ) { - - $url_params = $request->get_url_params(); - if ( isset( $url_params ) && isset( $url_params['user_id'] ) ) { - $url_params = ''; + if ( preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { + $ret['keyId'] = $matches[1]; + } + if ( preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { + $ret['(created)'] = $matches[1]; + } + if ( preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { + $ret['(expires)'] = $matches[1]; + } + if ( preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { + $ret['algorithm'] = $matches[1]; + } + if ( preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { + $ret['headers'] = explode( ' ', $matches[1] ); + } + if ( preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { + $ret['signature'] = base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); } - $strings = []; - $request_target = sprintf( - '%s %s%s', - strtolower($request->get_method()), - $request->get_route(), - $url_params - ); - - foreach ($headers as $value) { - if ( $value == '(request-target)' ) { - $strings[] = "$value: " . $request_target; - } else { - $strings[] = "$value: " . $request->get_header($value); + if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( !$ret['headers'] ) ) { + $ret['headers'] = ['date']; + } + + return $ret; + } + + public static function get_key( $keyId ) { + // If there was no key passed to verify, it will find the keyId and call this + // function to fetch the public key from stored data or a network fetch. + $actor = \strip_fragment_from_url( $keyId ); + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); + return rtrim( $publicKeyPem ); + } + + + 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 ( array_key_exists($header, $headers ) ) { + if ( $header === 'host' ) { + if ( isset( $headers['x_original_host'] ) ) { + $signed_data .= 'host: ' . $headers['x_original_host'][0] . "\n"; + } else { + $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + } + } else { + $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + } + } + if ( $header === '(created)' ) { + if ( !empty( $signature_block['(created)'] ) && intval( $signature_block['(created)'] ) > time() ) { + // created in future + return false; + } + $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; + } + if ( $header === '(expires)' ) { + if ( !empty( $signature_block['(expires)'] ) && intval( $signature_block['(expires)'] ) < time() ) { + // expired in past + return false; + } + $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; + } + if ( $header === 'content-type' ) { + $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; + } + if ( $header === 'date' ) { + // allow a bit of leeway for misconfigured clocks. + $d = new DateTime( $headers[$header][0] ); + $d->setTimeZone( new DateTimeZone('UTC') ); + + $dplus = time() + self::MAX_TIME_OFFSET; + $dminus = time() - self::MAX_TIME_OFFSET; + $c = wp_date( 'U' ); + + if ( $c > $dplus || $c < $dminus ) { + // time out of range + return false; + } } } - - return implode("\n", $strings); + // error_log( '$signed_data: ' . print_r( rtrim( $signed_data, "\n" ), true ) ); + return rtrim($signed_data, "\n"); } public static function generate_digest( $body ) { diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 2ad5ea2..c3ca058 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -84,7 +84,6 @@ class Inbox { // verify signature \Activitypub\Signature::verify_signature( $request ); - //\Activitypub\Signature::verify_signature( $headers, $key ); return $served; } @@ -340,6 +339,16 @@ class Inbox { }, ); + $params['validated'] = array( + 'sanitize_callback' => function( $param, $request, $key ) { + if ( \is_string( $param ) ) { + $param = array( $param ); + } + + return $param; + }, + ); + return $params; } From 504bbb99992879962b8bffec364f830b53c3a0d7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 1 Apr 2023 23:59:49 -0600 Subject: [PATCH 025/427] code style phpcs --- includes/class-signature.php | 104 +++++++++++++++++------------------ 1 file changed, 50 insertions(+), 54 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a4c45f4..d10309f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,8 +1,8 @@ get_headers(); - $headers["(request-target)"][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - if ( !$headers ) { + if ( ! $headers ) { $headers = self::default_server_headers(); } if ( array_key_exists( 'signature', $headers ) ) { @@ -131,13 +130,13 @@ class Signature { $signature_block = self::parse_signature_header( $headers['authorization'] ); } - if ( !$signature_block ) { + if ( ! $signature_block ) { return false; } $signed_headers = $signature_block['headers']; if ( ! $signed_headers ) { - $signed_headers = ['date']; + $signed_headers = array( 'date' ); } $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); @@ -150,37 +149,36 @@ class Signature { return false; } - if ( in_array( 'digest', $signed_headers ) && isset( $body ) ) { + if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { $digest = explode( '=', $headers['digest'], 2 ); - if ( $digest[0] === 'SHA-256' ) { + if ( 'SHA-256' === $digest[0] ) { $hashalg = 'sha256'; } - if ( $digest[0] === 'SHA-512' ) { + if ( 'SHA-512' === $digest[0] ) { $hashalg = 'sha512'; } - // TODO Test - if ( base64_encode( hash( $hashalg, $body, true ) ) !== $digest[1] ) { + if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore return false; } } - $public_key = $key?? self::get_key( $signature_block['keyId'] ); + $public_key = isset( $key ) ? $key : self::get_key( $signature_block['keyId'] ); - return \openssl_verify( $signed_data,$signature_block['signature'], $public_key, $algorithm ) > 0; + return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; } public static function default_server_headers() { - $headers = array( + $headers = array( '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], 'content-type' => $_SERVER['CONTENT_TYPE'], 'content-length' => $_SERVER['CONTENT_LENGTH'], ); foreach ( $_SERVER as $k => $v ) { - if ( strpos( $k, 'HTTP_' ) === 0 ) { - $field = str_replace( '_', '-', strtolower( substr( $k, 5 ) ) ); - $headers[$field] = $v; + if ( \strpos( $k, 'HTTP_' ) === 0 ) { + $field = \str_replace( '_', '-', \strtolower( \substr( $k, 5 ) ) ); + $headers[ $field ] = $v; } } return $headers; @@ -199,86 +197,85 @@ class Signature { } public static function parse_signature_header( $header ) { - $ret = []; - $matches = []; - $h_string = implode( ',', (array) $header[0] ); + $ret = array(); + $matches = array(); + $h_string = \implode( ',', (array) $header[0] ); - if ( preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { + if ( \preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { $ret['keyId'] = $matches[1]; } - if ( preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { + if ( \preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { $ret['(created)'] = $matches[1]; } - if ( preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { + if ( \preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { $ret['(expires)'] = $matches[1]; } - if ( preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { + if ( \preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { $ret['algorithm'] = $matches[1]; } - if ( preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { - $ret['headers'] = explode( ' ', $matches[1] ); + if ( \preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { + $ret['headers'] = \explode( ' ', $matches[1] ); } - if ( preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { - $ret['signature'] = base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); + if ( \preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { + $ret['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore } - if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( !$ret['headers'] ) ) { - $ret['headers'] = ['date']; + if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( ! $ret['headers'] ) ) { + $ret['headers'] = array( 'date' ); } return $ret; } - public static function get_key( $keyId ) { + public static function get_key( $keyId ) { // phpcs:ignore // If there was no key passed to verify, it will find the keyId and call this // function to fetch the public key from stored data or a network fetch. - $actor = \strip_fragment_from_url( $keyId ); - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); - return rtrim( $publicKeyPem ); + $actor = \strip_fragment_from_url( $keyId ); // phpcs:ignore + $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore + return \rtrim( $publicKeyPem ); // phpcs:ignore } 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 ( array_key_exists($header, $headers ) ) { - if ( $header === 'host' ) { + if ( \array_key_exists( $header, $headers ) ) { + if ( 'host' === $header ) { if ( isset( $headers['x_original_host'] ) ) { - $signed_data .= 'host: ' . $headers['x_original_host'][0] . "\n"; + $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; } else { - $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } } else { - $signed_data .= $header . ': ' . $headers[$header][0] . "\n"; + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } } - if ( $header === '(created)' ) { - if ( !empty( $signature_block['(created)'] ) && intval( $signature_block['(created)'] ) > time() ) { + if ( '(created)' === $header ) { + if ( ! \empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; } - if ( $header === '(expires)' ) { - if ( !empty( $signature_block['(expires)'] ) && intval( $signature_block['(expires)'] ) < time() ) { + if ( '(expires)' === $header ) { + if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) { // expired in past return false; } $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; } - if ( $header === 'content-type' ) { + if ( 'content-type' === $header ) { $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; } - if ( $header === 'date' ) { + if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. - $d = new DateTime( $headers[$header][0] ); - $d->setTimeZone( new DateTimeZone('UTC') ); + $d = new DateTime( $headers[ $header ][0] ); + $d->setTimeZone( new DateTimeZone( 'UTC' ) ); + $c = $d->format( 'U' ); - $dplus = time() + self::MAX_TIME_OFFSET; - $dminus = time() - self::MAX_TIME_OFFSET; - $c = wp_date( 'U' ); + $dplus = time() + ( 3 * HOUR_IN_SECONDS ); + $dminus = time() - ( 3 * HOUR_IN_SECONDS ); if ( $c > $dplus || $c < $dminus ) { // time out of range @@ -286,8 +283,7 @@ class Signature { } } } - // error_log( '$signed_data: ' . print_r( rtrim( $signed_data, "\n" ), true ) ); - return rtrim($signed_data, "\n"); + return \rtrim( $signed_data, "\n" ); } public static function generate_digest( $body ) { From 9ec09c540716b7c2c3ab797f17d0d5fa4497c829 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 00:12:02 -0600 Subject: [PATCH 026/427] remove unneeded dependencies --- activitypub.php | 2 -- composer.json | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/activitypub.php b/activitypub.php index f8633e8..7320f0c 100644 --- a/activitypub.php +++ b/activitypub.php @@ -15,8 +15,6 @@ namespace Activitypub; -require __DIR__ . '/vendor/autoload.php'; - /** * Initialize plugin */ diff --git a/composer.json b/composer.json index e9cb057..67bd2d3 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,7 @@ "type": "wordpress-plugin", "require": { "php": ">=5.6.0", - "composer/installers": "^1.0 || ^2.0", - "phpseclib/phpseclib": "~3.0" + "composer/installers": "^1.0 || ^2.0" }, "require-dev": { "phpunit/phpunit": "^5.7.21 || ^6.5 || ^7.5 || ^8", From 90b45438b20202ce3f491712d2d7bf23b7b0322e Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 00:30:17 -0600 Subject: [PATCH 027/427] cleanup --- includes/class-signature.php | 7 +------ includes/functions.php | 5 ----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index d10309f..a25d939 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -11,11 +11,6 @@ use DateTimeZone; */ class Signature { - /** - * How much leeway to provide on the date header in seconds. - * Not everybody uses NTP. - */ - const DEFAULT_SIGNING_ALGORITHM = 'sha256'; /** @@ -252,7 +247,7 @@ class Signature { } } if ( '(created)' === $header ) { - if ( ! \empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { + if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } diff --git a/includes/functions.php b/includes/functions.php index 9df6311..8f2e5d9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -200,11 +200,6 @@ function get_inbox_by_actor( $actor ) { */ function get_publickey_by_actor( $actor, $key_id ) { $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - //Other Implementations may include an 'operations' query (Zap) - if ( isset( $metadata['publicKey']['id'] ) ) { - $metadata['publicKey']['id'] = strtok($metadata['publicKey']['id'], "?"); - } if ( \is_wp_error( $metadata ) ) { return $metadata; From 2293c0b3d776e90a92dfa536a52f05fbff8b2068 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 16:38:39 -0600 Subject: [PATCH 028/427] use verify_http_signature in validate_callback rename verify_signature --- includes/class-signature.php | 2 +- includes/rest/class-inbox.php | 31 +++++++++++++------------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a25d939..1ef447b 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,7 +112,7 @@ class Signature { } } - public static function verify_signature( $request = null ) { + public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index c3ca058..0869533 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -74,17 +74,10 @@ class Inbox { return $served; } - $signature = $request->get_header( 'signature' ); - - if ( ! $signature ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { return $served; } - $headers = $request->get_headers(); - - // verify signature - \Activitypub\Signature::verify_signature( $request ); - return $served; } @@ -237,6 +230,12 @@ class Inbox { $params['id'] = array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return false; + } + return $param; + }, ); $params['actor'] = array( @@ -281,6 +280,12 @@ class Inbox { 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', + 'validate_callback' => function( $param, $request, $key ) { + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return false; + } + return $param; + }, ); $params['actor'] = array( @@ -339,16 +344,6 @@ class Inbox { }, ); - $params['validated'] = array( - 'sanitize_callback' => function( $param, $request, $key ) { - if ( \is_string( $param ) ) { - $param = array( $param ); - } - - return $param; - }, - ); - return $params; } From d6169f4bc3f111143d73773eb0ee4c3446d45da3 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 2 Apr 2023 20:38:10 -0600 Subject: [PATCH 029/427] Add content-length header if present in sig headers --- includes/class-signature.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 1ef447b..96845d4 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -263,6 +263,9 @@ class Signature { if ( 'content-type' === $header ) { $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; } + if ( 'content-length' === $header ) { + $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; + } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. $d = new DateTime( $headers[ $header ][0] ); From 502bf8b5a6ef55d9a0bdb50bb85f75230bac567a Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 19:58:08 -0600 Subject: [PATCH 030/427] Get actor from key with non-standard uri --- includes/class-signature.php | 2 +- includes/functions.php | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 96845d4..17a9a6f 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -225,7 +225,7 @@ class Signature { public static function get_key( $keyId ) { // phpcs:ignore // If there was no key passed to verify, it will find the keyId and call this // function to fetch the public key from stored data or a network fetch. - $actor = \strip_fragment_from_url( $keyId ); // phpcs:ignore + $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore return \rtrim( $publicKeyPem ); // phpcs:ignore } diff --git a/includes/functions.php b/includes/functions.php index 8f2e5d9..f731190 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,6 +219,15 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } +function get_actor_from_key( $key_id ) { + $actor = \strip_fragment_from_url( $key_id ); + if ( $actor === $key_id ) { + // strip /main-key/ for GoToSocial. + $actor = \dirname( $key_id, 1 ); + } + return $actor; +} + function get_follower_inboxes( $user_id ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $inboxes = array(); From 9eb903ac15c9e813d3ccb6e4adf159c1bc8929f8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 20:33:00 -0600 Subject: [PATCH 031/427] phpcs compat --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index f731190..818f9f3 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -223,7 +223,7 @@ function get_actor_from_key( $key_id ) { $actor = \strip_fragment_from_url( $key_id ); if ( $actor === $key_id ) { // strip /main-key/ for GoToSocial. - $actor = \dirname( $key_id, 1 ); + $actor = \dirname( $key_id ); } return $actor; } From 3a0f62b092208ad87c15bc850b2de66335803e4f Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 4 Apr 2023 20:36:25 -0600 Subject: [PATCH 032/427] phpcs --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 818f9f3..751e493 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -221,7 +221,7 @@ function get_publickey_by_actor( $actor, $key_id ) { function get_actor_from_key( $key_id ) { $actor = \strip_fragment_from_url( $key_id ); - if ( $actor === $key_id ) { + if ( $actor === $key_id ) { // strip /main-key/ for GoToSocial. $actor = \dirname( $key_id ); } From e1722cd4d393b2e60bf7abf8868345bc7a9d0f74 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 5 Apr 2023 13:25:39 -0600 Subject: [PATCH 033/427] Simplify signature_algorithm --- includes/class-signature.php | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 17a9a6f..3c28941 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -11,8 +11,6 @@ use DateTimeZone; */ class Signature { - const DEFAULT_SIGNING_ALGORITHM = 'sha256'; - /** * @param int $user_id * @@ -181,12 +179,10 @@ class Signature { public static function get_signature_algorithm( $signature_block ) { switch ( $signature_block['algorithm'] ) { - case 'rsa-sha256': - return 'sha256'; case 'rsa-sha-512': return 'sha512'; - case 'hs2019': - return self::DEFAULT_SIGNING_ALGORITHM; + default: + return 'sha256'; } return false; } @@ -223,8 +219,6 @@ class Signature { } public static function get_key( $keyId ) { // phpcs:ignore - // If there was no key passed to verify, it will find the keyId and call this - // function to fetch the public key from stored data or a network fetch. $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore return \rtrim( $publicKeyPem ); // phpcs:ignore From 643c47dcb7644af10d6687e4a119a4c7c6fa7454 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 10 Apr 2023 13:10:46 +0200 Subject: [PATCH 034/427] Webfinger info: avoid PHP warning when user isn't defined This should avoid warnings like this one: ``` PHP Warning: Attempt to read property "user_login" on bool in /var/www/html/wp-content/plugins/activitypub/includes/class-webfinger.php on line 27 ``` --- includes/class-webfinger.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index ab33411..c7d2cba 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -23,6 +23,9 @@ class Webfinger { } $user = \get_user_by( 'id', $user_id ); + if ( ! $user ) { + return ''; + } return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } From 30d78417d87c5e4765e7c89cac7ffb3ee4c4cccf Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 14 Apr 2023 23:53:43 -0600 Subject: [PATCH 035/427] Fixes key retrieval --- includes/class-signature.php | 15 ++++++--------- includes/rest/class-inbox.php | 10 +++++----- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 3c28941..84cf612 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,6 +112,7 @@ class Signature { public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : '' ; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { @@ -123,7 +124,7 @@ class Signature { $signature_block = self::parse_signature_header( $headers['authorization'] ); } - if ( ! $signature_block ) { + if ( ! isset( $signature_block ) || ! $signature_block ) { return false; } @@ -143,6 +144,9 @@ class Signature { } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { + if ( is_array( $headers['digest'] ) ) { + $headers['digest'] = $headers['digest'][0]; + } $digest = explode( '=', $headers['digest'], 2 ); if ( 'SHA-256' === $digest[0] ) { $hashalg = 'sha256'; @@ -156,7 +160,7 @@ class Signature { } } - $public_key = isset( $key ) ? $key : self::get_key( $signature_block['keyId'] ); + $public_key = \rtrim( \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ) ); // phpcs:ignore return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; @@ -218,13 +222,6 @@ class Signature { return $ret; } - public static function get_key( $keyId ) { // phpcs:ignore - $actor = \Activitypub\get_actor_from_key( $keyId ); // phpcs:ignore - $publicKeyPem = \Activitypub\get_publickey_by_actor( $actor, $keyId ); // phpcs:ignore - return \rtrim( $publicKeyPem ); // phpcs:ignore - } - - 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. diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 0869533..a93d064 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -74,10 +74,6 @@ class Inbox { return $served; } - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return $served; - } - return $served; } @@ -230,9 +226,13 @@ class Inbox { $params['id'] = array( 'required' => true, 'sanitize_callback' => 'esc_url_raw', + ); + + $params['signature'] = array( + 'required' => true, 'validate_callback' => function( $param, $request, $key ) { if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return false; + return false; // returns http 400 rest_invalid_param } return $param; }, From 590c990e218b1bc60a77f8397bd7016c580bbf39 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 14 Apr 2023 23:59:04 -0600 Subject: [PATCH 036/427] phpcs --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 84cf612..c6036bd 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,7 +112,7 @@ class Signature { public static function verify_http_signature( $request = null ) { $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : '' ; + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { From c32eec2390328fb37b320fc31f8a74c6b1ac1f4d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Apr 2023 15:22:11 +0200 Subject: [PATCH 037/427] some code cleanup --- activitypub.php | 30 +++++++++++++------------- includes/class-activity-dispatcher.php | 30 +++++++++++++++----------- includes/class-activitypub.php | 12 +++++------ includes/class-admin.php | 16 +++++++------- includes/class-debug.php | 5 ++++- includes/class-hashtag.php | 4 ++-- includes/class-health-check.php | 8 +++---- includes/class-mention.php | 6 +++--- includes/class-webfinger.php | 10 +++++---- includes/model/class-activity.php | 2 ++ includes/rest/class-followers.php | 4 ++-- includes/rest/class-following.php | 4 ++-- includes/rest/class-inbox.php | 24 +++++++++++---------- includes/rest/class-nodeinfo.php | 12 +++++------ includes/rest/class-outbox.php | 4 ++-- includes/rest/class-webfinger.php | 17 +++++++++------ 16 files changed, 102 insertions(+), 86 deletions(-) diff --git a/activitypub.php b/activitypub.php index 7320f0c..35e6de8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -39,50 +39,50 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; - \Activitypub\Activity_Dispatcher::init(); + Activity_Dispatcher::init(); require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php'; - \Activitypub\Activitypub::init(); + Activitypub::init(); // Configure the REST API route require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php'; - \Activitypub\Rest\Outbox::init(); + Rest\Outbox::init(); require_once \dirname( __FILE__ ) . '/includes/rest/class-inbox.php'; - \Activitypub\Rest\Inbox::init(); + Rest\Inbox::init(); require_once \dirname( __FILE__ ) . '/includes/rest/class-followers.php'; - \Activitypub\Rest\Followers::init(); + Rest\Followers::init(); require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php'; - \Activitypub\Rest\Following::init(); + Rest\Following::init(); require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php'; - \Activitypub\Rest\Webfinger::init(); + Rest\Webfinger::init(); // load NodeInfo endpoints only if blog is public if ( true === (bool) \get_option( 'blog_public', 1 ) ) { require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php'; - \Activitypub\Rest\NodeInfo::init(); + Rest\NodeInfo::init(); } require_once \dirname( __FILE__ ) . '/includes/class-admin.php'; - \Activitypub\Admin::init(); + Admin::init(); require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php'; - \Activitypub\Hashtag::init(); + Hashtag::init(); require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php'; - \Activitypub\Shortcodes::init(); + Shortcodes::init(); require_once \dirname( __FILE__ ) . '/includes/class-mention.php'; - \Activitypub\Mention::init(); + Mention::init(); require_once \dirname( __FILE__ ) . '/includes/class-debug.php'; - \Activitypub\Debug::init(); + Debug::init(); require_once \dirname( __FILE__ ) . '/includes/class-health-check.php'; - \Activitypub\Health_Check::init(); + Health_Check::init(); if ( \WP_DEBUG ) { require_once \dirname( __FILE__ ) . '/includes/debug.php'; @@ -136,6 +136,6 @@ function flush_rewrite_rules() { */ function enable_buddypress_features() { require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; - \Activitypub\Integration\Buddypress::init(); + Integration\Buddypress::init(); } add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 6df83e7..57fee1a 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -1,6 +1,9 @@ get_post_author(); - $activitypub_activity = new \Activitypub\Model\Activity( $activity_type ); + $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 4a60915..4730515 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -11,9 +11,9 @@ class Activitypub { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 ); - \add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) ); - \add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 ); + \add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 ); + \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) ); + \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 ); // Add support for ActivityPub to custom post types $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); @@ -22,9 +22,9 @@ class Activitypub { \add_post_type_support( $post_type, 'activitypub' ); } - \add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 33, 3 ); - \add_action( 'wp_trash_post', array( '\Activitypub\Activitypub', 'trash_post' ), 1 ); - \add_action( 'untrash_post', array( '\Activitypub\Activitypub', 'untrash_post' ), 1 ); + \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); + \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 ); + \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); } /** diff --git a/includes/class-admin.php b/includes/class-admin.php index e98d547..220ed9b 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -11,10 +11,10 @@ class Admin { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'admin_menu', array( '\Activitypub\Admin', 'admin_menu' ) ); - \add_action( 'admin_init', array( '\Activitypub\Admin', 'register_settings' ) ); - \add_action( 'show_user_profile', array( '\Activitypub\Admin', 'add_fediverse_profile' ) ); - \add_action( 'admin_enqueue_scripts', array( '\Activitypub\Admin', 'enqueue_scripts' ) ); + \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); + \add_action( 'admin_init', array( self::class, 'register_settings' ) ); + \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) ); + \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } /** @@ -26,14 +26,14 @@ class Admin { 'ActivityPub', 'manage_options', 'activitypub', - array( '\Activitypub\Admin', 'settings_page' ) + array( self::class, 'settings_page' ) ); - \add_action( 'load-' . $settings_page, array( '\Activitypub\Admin', 'add_settings_help_tab' ) ); + \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) ); - $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( '\Activitypub\Admin', 'followers_list_page' ) ); + $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); - \add_action( 'load-' . $followers_list_page, array( '\Activitypub\Admin', 'add_followers_list_help_tab' ) ); + \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); } /** diff --git a/includes/class-debug.php b/includes/class-debug.php index 767d4a9..36f8bda 100644 --- a/includes/class-debug.php +++ b/includes/class-debug.php @@ -1,6 +1,9 @@ \__( 'Author URL test', 'activitypub' ), - 'test' => array( '\Activitypub\Health_Check', 'test_author_url' ), + 'test' => array( self::class, 'test_author_url' ), ); $tests['direct']['activitypub_test_webfinger'] = array( 'label' => __( 'WebFinger Test', 'activitypub' ), - 'test' => array( '\Activitypub\Health_Check', 'test_webfinger' ), + 'test' => array( self::class, 'test_webfinger' ), ); return $tests; diff --git a/includes/class-mention.php b/includes/class-mention.php index 7c8672a..e0930cc 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -11,8 +11,8 @@ class Mention { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_filter( 'the_content', array( '\Activitypub\Mention', 'the_content' ), 99, 2 ); - \add_filter( 'activitypub_extract_mentions', array( '\Activitypub\Mention', 'extract_mentions' ), 99, 2 ); + \add_filter( 'the_content', array( self::class, 'the_content' ), 99, 2 ); + \add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 ); } /** @@ -46,7 +46,7 @@ class Mention { $the_content ); - $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( '\Activitypub\Mention', 'replace_with_links' ), $the_content ); + $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content ); $the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index c7d2cba..1581853 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -1,6 +1,8 @@ \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Followers', 'get' ), + 'callback' => array( self::class, 'get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', ), diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index d7caff4..52a95e7 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -13,7 +13,7 @@ class Following { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'rest_api_init', array( '\Activitypub\Rest\Following', 'register_routes' ) ); + \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); } /** @@ -26,7 +26,7 @@ class Following { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Following', 'get' ), + 'callback' => array( self::class, 'get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', ), diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 86aefd4..1a63108 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -1,6 +1,8 @@ \WP_REST_Server::EDITABLE, - 'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ), + 'callback' => array( self::class, 'shared_inbox_post' ), 'args' => self::shared_inbox_post_parameters(), 'permission_callback' => '__return_true', ), @@ -45,13 +47,13 @@ class Inbox { array( array( 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ), + 'callback' => array( self::class, 'user_inbox_post' ), 'args' => self::user_inbox_post_parameters(), 'permission_callback' => '__return_true', ), array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ), + 'callback' => array( self::class, 'user_inbox_get' ), 'args' => self::user_inbox_get_parameters(), 'permission_callback' => '__return_true', ), @@ -356,7 +358,7 @@ class Inbox { $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); // send "Accept" activity - $activity = new \Activitypub\Model\Activity( 'Accept' ); + $activity = new Activity( 'Accept' ); $activity->set_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); $activity->set_to( $object['actor'] ); diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 3106c5e..100fdd3 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -13,9 +13,9 @@ class Nodeinfo { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'rest_api_init', array( '\Activitypub\Rest\Nodeinfo', 'register_routes' ) ); - \add_filter( 'nodeinfo_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo_discovery' ), 10, 2 ); - \add_filter( 'nodeinfo2_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo2_discovery' ), 10 ); + \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); + \add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_discovery' ), 10, 2 ); + \add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_discovery' ), 10 ); } /** @@ -28,7 +28,7 @@ class Nodeinfo { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'discovery' ), + 'callback' => array( self::class, 'discovery' ), 'permission_callback' => '__return_true', ), ) @@ -40,7 +40,7 @@ class Nodeinfo { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo' ), + 'callback' => array( self::class, 'nodeinfo' ), 'permission_callback' => '__return_true', ), ) @@ -52,7 +52,7 @@ class Nodeinfo { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo2' ), + 'callback' => array( self::class, 'nodeinfo2' ), 'permission_callback' => '__return_true', ), ) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 8749d11..905dfd5 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -13,7 +13,7 @@ class Outbox { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_action( 'rest_api_init', array( '\Activitypub\Rest\Outbox', 'register_routes' ) ); + \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); } /** @@ -26,7 +26,7 @@ class Outbox { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox_get' ), + 'callback' => array( self::class, 'user_outbox_get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', ), diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 0c6d5f1..d41cf25 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -1,6 +1,9 @@ \WP_REST_Server::READABLE, - 'callback' => array( '\Activitypub\Rest\Webfinger', 'webfinger' ), + 'callback' => array( self::class, 'webfinger' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', ), @@ -45,7 +48,7 @@ class Webfinger { $resource = $request->get_param( 'resource' ); if ( \strpos( $resource, '@' ) === false ) { - return new \WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); + return new WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); } $resource = \str_replace( 'acct:', '', $resource ); @@ -54,13 +57,13 @@ class Webfinger { $resource_host = \substr( \strrchr( $resource, '@' ), 1 ); if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) { - return new \WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); + return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); } $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); if ( ! $user || ! user_can( $user, 'publish_posts' ) ) { - return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); + return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); } $json = array( @@ -82,7 +85,7 @@ class Webfinger { ), ); - return new \WP_REST_Response( $json, 200 ); + return new WP_REST_Response( $json, 200 ); } /** From eeb3ba295274f43b1776ad4e7953525327fa80f5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Apr 2023 15:32:38 +0200 Subject: [PATCH 038/427] remove unused "use function" --- includes/model/class-activity.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index cfbb431..34f6146 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -1,8 +1,6 @@ Date: Fri, 21 Apr 2023 08:42:51 +0200 Subject: [PATCH 039/427] count only users that can `publish_posts` --- includes/rest/class-nodeinfo.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 100fdd3..980c24b 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -75,13 +75,24 @@ class Nodeinfo { 'version' => \get_bloginfo( 'version' ), ); - $users = \count_users(); + $users = \get_users( + array( + 'capability__in' => array( 'publish_posts' ), + ) + ); + + if ( is_array( $users ) ) { + $users = count( $users ); + } else { + $users = 1; + } + $posts = \wp_count_posts(); $comments = \wp_count_comments(); $nodeinfo['usage'] = array( 'users' => array( - 'total' => (int) $users['total_users'], + 'total' => $users, ), 'localPosts' => (int) $posts->publish, 'localComments' => (int) $comments->approved, From 5faddba5114af24abf4764f0e82fbeab63d0cf3a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 08:51:25 +0200 Subject: [PATCH 040/427] this function should not work without $request --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c6036bd..f0ca349 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -110,7 +110,7 @@ class Signature { } } - public static function verify_http_signature( $request = null ) { + public static function verify_http_signature( $request ) { $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); From a8b963ec26102fc8ac5bbfbd479d5de470dd5956 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 08:51:38 +0200 Subject: [PATCH 041/427] some code cleanups --- includes/rest/class-inbox.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index c811605..4327587 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -1,7 +1,10 @@ header( 'Content-Type', 'application/activity+json' ); @@ -140,7 +143,7 @@ class Inbox { \do_action( 'activitypub_inbox', $data, $user_id, $type ); \do_action( "activitypub_inbox_{$type}", $data, $user_id ); - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -179,7 +182,7 @@ class Inbox { \do_action( "activitypub_inbox_{$type}", $data, $user->ID ); } - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -233,7 +236,7 @@ class Inbox { $params['signature'] = array( 'required' => true, 'validate_callback' => function( $param, $request, $key ) { - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + if ( ! Signature::verify_http_signature( $request ) ) { return false; // returns http 400 rest_invalid_param } return $param; @@ -283,7 +286,7 @@ class Inbox { 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', 'validate_callback' => function( $param, $request, $key ) { - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + if ( ! Signature::verify_http_signature( $request ) ) { return false; } return $param; @@ -357,7 +360,7 @@ class Inbox { */ public static function handle_follow( $object, $user_id ) { // save follower - \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); + Followers::add_follower( $object['actor'], $user_id ); // get inbox $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); @@ -382,7 +385,7 @@ class Inbox { */ public static function handle_unfollow( $object, $user_id ) { if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) { - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); + Followers::remove_follower( $object['actor'], $user_id ); } } From 7769d76849d6aa1a72d5ca51868679d0c1f6a2f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 14:56:22 +0200 Subject: [PATCH 042/427] use a taxonomy to save the list of followers --- activitypub.php | 5 +- includes/class-admin.php | 2 +- includes/collection/class-followers.php | 244 ++++++++++++++++++++++++ includes/rest/class-inbox.php | 2 - includes/table/class-followers.php | 84 ++++++++ includes/table/followers-list.php | 36 ---- templates/followers-list.php | 6 +- 7 files changed, 336 insertions(+), 43 deletions(-) create mode 100644 includes/collection/class-followers.php create mode 100644 includes/table/class-followers.php delete mode 100644 includes/table/followers-list.php diff --git a/activitypub.php b/activitypub.php index 35e6de8..c1cb823 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,7 +29,7 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); - require_once \dirname( __FILE__ ) . '/includes/table/followers-list.php'; + require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; @@ -44,6 +44,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php'; Activitypub::init(); + require_once \dirname( __FILE__ ) . '/includes/collection/class-followers.php'; + Collection\Followers::init(); + // Configure the REST API route require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php'; Rest\Outbox::init(); diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..fbb953a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -31,7 +31,7 @@ class Admin { \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) ); - $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); + $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php new file mode 100644 index 0000000..7d7f8eb --- /dev/null +++ b/includes/collection/class-followers.php @@ -0,0 +1,244 @@ + array( + 'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ), + 'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ), + 'menu_name' => __( 'Followers', 'activitypub' ), + ), + 'hierarchical' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_nav_menus' => false, + 'show_admin_column' => false, + 'query_var' => false, + 'rewrite' => false, + 'public' => false, + 'capabilities' => array( + 'edit_terms' => null, + ), + ); + + register_taxonomy( self::TAXONOMY, 'user', $args ); + register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); + + register_term_meta( + self::TAXONOMY, + 'user_id', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_username' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'name', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_displayname' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'username', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_username' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'avatar', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_avatar' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'created_at', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_created_at' ), + ) + ); + + register_term_meta( + self::TAXONOMY, + 'inbox', + array( + 'type' => 'string', + 'single' => true, + //'sanitize_callback' => array( self::class, 'validate_created_at' ), + ) + ); + + do_action( 'activitypub_after_register_taxonomy' ); + } + + /** + * Handle the "Follow" Request + * + * @param array $object The JSON "Follow" Activity + * @param int $user_id The ID of the WordPress User + * + * @return void + */ + public static function handle_follow_request( $object, $user_id ) { + // save follower + self::add_follower( $user_id, $object['actor'] ); + } + + /** + * Handles "Unfollow" requests + * + * @param array $object The JSON "Undo" Activity + * @param int $user_id The ID of the WordPress User + */ + public static function handle_undo_request( $object, $user_id ) { + if ( + isset( $object['object'] ) && + isset( $object['object']['type'] ) && + 'Follow' === $object['object']['type'] + ) { + self::remove_follower( $user_id, $object['actor'] ); + } + } + + /** + * Undocumented function + * + * @return void + */ + public static function add_follower( $user_id, $actor ) { + $remote_data = get_remote_metadata_by_actor( $actor ); + + if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { + $remote_data = array(); + } + + $term = term_exists( $actor, self::TAXONOMY ); + + if ( ! $term ) { + $term = wp_insert_term( + $actor, + self::TAXONOMY, + array( + 'slug' => sanitize_title( $actor ), + 'description' => wp_json_encode( $remote_data ), + ) + ); + } + + $term_id = $term['term_id']; + + $map_meta = array( + 'name' => 'name', + 'preferredUsername' => 'username', + 'inbox' => 'inbox', + ); + + foreach ( $map_meta as $remote => $internal ) { + if ( ! empty( $remote_data[ $remote ] ) ) { + update_term_meta( $term_id, $internal, esc_html( $remote_data[ $remote ] ), true ); + } + } + + if ( ! empty( $remote_data['icon']['url'] ) ) { + update_term_meta( $term_id, 'avatar', esc_url_raw( $remote_data['icon']['url'] ), true ); + } + + wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); + } + + public static function send_ack() { + // get inbox + $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); + + // send "Accept" activity + $activity = new Activity( 'Accept' ); + $activity->set_object( $object ); + $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity->set_to( $object['actor'] ); + $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); + + $activity = $activity->to_simple_json(); + $response = safe_remote_post( $inbox, $activity, $user_id ); + } + + public static function get_followers( $user_id, $number = null, $offset = null ) { + //self::migrate_followers( $user_id ); + + $terms = new WP_Term_Query( + array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => $number, + 'offset' => $offset, + ) + ); + + return $terms->get_terms(); + } + + public static function count_followers( $user_id ) { + return count( self::get_followers( $user_id ) ); + } + + public static function migrate_followers( $user_id ) { + $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + + if ( $followes ) { + foreach ( $followes as $follower ) { + self::add_follower( $user_id, $follower ); + } + } + } +} diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 1a63108..761bfd6 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,8 +17,6 @@ class Inbox { public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); - \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); - \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 ); \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php new file mode 100644 index 0000000..bba3208 --- /dev/null +++ b/includes/table/class-followers.php @@ -0,0 +1,84 @@ + '', + 'avatar' => \__( 'Avatar', 'activitypub' ), + 'name' => \__( 'Name', 'activitypub' ), + 'username' => \__( 'Username', 'activitypub' ), + 'identifier' => \__( 'Identifier', 'activitypub' ), + ); + } + + public function get_sortable_columns() { + return array(); + } + + public function prepare_items() { + $columns = $this->get_columns(); + $hidden = array(); + + $this->process_action(); + $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); + + $page_num = $this->get_pagenum(); + $per_page = 20; + + $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( \get_current_user_id() ); + + $this->items = array(); + $this->set_pagination_args( + array( + 'total_items' => $counter, + 'total_pages' => round( $counter / $per_page ), + 'per_page' => $per_page, + ) + ); + + foreach ( $follower as $follower ) { + $item = array( + 'avatar' => esc_attr( get_term_meta( $follower->term_id, 'avatar', true ) ), + 'name' => esc_attr( get_term_meta( $follower->term_id, 'name', true ) ), + 'username' => esc_attr( get_term_meta( $follower->term_id, 'username', true ) ), + 'identifier' => esc_attr( $follower->name ), + ); + + $this->items[] = $item; + } + } + + public function get_bulk_actions() { + return array( + 'revoke' => __( 'Revoke', 'activitypub' ), + 'verify' => __( 'Verify', 'activitypub' ), + ); + } + + public function column_default( $item, $column_name ) { + if ( ! array_key_exists( $column_name, $item ) ) { + return __( 'None', 'activitypub' ); + } + return $item[ $column_name ]; + } + + public function column_avatar( $item ) { + return sprintf( + '', + $item['avatar'] + ); + } + + public function column_cb( $item ) { + return sprintf( '', esc_attr( $item['identifier'] ) ); + } +} diff --git a/includes/table/followers-list.php b/includes/table/followers-list.php deleted file mode 100644 index 81444ee..0000000 --- a/includes/table/followers-list.php +++ /dev/null @@ -1,36 +0,0 @@ - \__( 'Identifier', 'activitypub' ), - ); - } - - public function get_sortable_columns() { - return array(); - } - - public function prepare_items() { - $columns = $this->get_columns(); - $hidden = array(); - - $this->process_action(); - $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); - - $this->items = array(); - - foreach ( \Activitypub\Peer\Followers::get_followers( \get_current_user_id() ) as $follower ) { - $this->items[]['identifier'] = \esc_attr( $follower ); - } - } - - public function column_default( $item, $column_name ) { - return $item[ $column_name ]; - } -} diff --git a/templates/followers-list.php b/templates/followers-list.php index 057f498..261808f 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,10 +1,10 @@
-

+

-

+

- +
From 038bf25b2e186b36e2f4292ec6ec8f0508fb87b4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sat, 15 Apr 2023 23:57:08 -0600 Subject: [PATCH 043/427] remove guessing function --- includes/functions.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 95ed09e..e00f9e0 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,16 +219,7 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } -function get_actor_from_key( $key_id ) { - $actor = \strip_fragment_from_url( $key_id ); - if ( $actor === $key_id ) { - // strip /main-key/ for GoToSocial. - $actor = \dirname( $key_id ); - } - return $actor; -} - -function get_follower_inboxes( $user_id, $cc = array() ) { +function get_follower_inboxes( $user_id ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = array_merge( $followers, $cc ); $followers = array_unique( $followers ); From bb21803b18464ea8f5863667409d45a3359e8e80 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 16 Apr 2023 13:46:44 -0600 Subject: [PATCH 044/427] Add Secure mode setting --- includes/class-admin.php | 9 +++++++++ templates/settings.php | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..dfe7deb 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -142,6 +142,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); + \register_setting( + 'activitypub', + 'activitypub_use_secure_mode', + array( + 'type' => 'boolean', + 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + 'default' => 0, + ) + ); } public static function add_settings_help_tab() { diff --git a/templates/settings.php b/templates/settings.php index 2bfaed4..6429688 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,6 +171,21 @@ + + + + + + + +
+ + +

+ +

+
+ From 036ee3180b54c9f63c531811ebef86f7d99a9a78 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 07:46:14 -0600 Subject: [PATCH 045/427] move signature verification to callback --- includes/rest/class-inbox.php | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4327587..8a1f139 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -134,6 +134,11 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { + // SecureMode/Authorized fetch. + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return new \WP_REST_Response( array(), 403 ); + } + $user_id = $request->get_param( 'user_id' ); $data = $request->get_params(); @@ -154,6 +159,11 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { + // SecureMode/Authorized fetch. + if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { + return new \WP_REST_Response( array(), 403 ); + } + $data = $request->get_params(); $type = $request->get_param( 'type' ); $users = self::extract_recipients( $data ); @@ -233,16 +243,6 @@ class Inbox { 'sanitize_callback' => 'esc_url_raw', ); - $params['signature'] = array( - 'required' => true, - 'validate_callback' => function( $param, $request, $key ) { - if ( ! Signature::verify_http_signature( $request ) ) { - return false; // returns http 400 rest_invalid_param - } - return $param; - }, - ); - $params['actor'] = array( 'required' => true, 'sanitize_callback' => function( $param, $request, $key ) { @@ -285,12 +285,6 @@ class Inbox { 'required' => true, 'type' => 'string', 'sanitize_callback' => 'esc_url_raw', - 'validate_callback' => function( $param, $request, $key ) { - if ( ! Signature::verify_http_signature( $request ) ) { - return false; - } - return $param; - }, ); $params['actor'] = array( From 75e9b1e2819726faf7cde87953fd3fd2485041c6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:21 +0200 Subject: [PATCH 046/427] deprecate old functions --- includes/peer/class-followers.php | 68 +++++-------------------------- 1 file changed, 11 insertions(+), 57 deletions(-) diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index abc18e3..d5caf10 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -9,76 +9,30 @@ namespace Activitypub\Peer; class Followers { public static function get_followers( $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' ); - if ( ! $followers ) { - return array(); + $items = array(); // phpcs:ignore + foreach ( \Activitypub\Collection\Followers::get_followers( $author_id ) as $follower ) { + $items[] = $follower->name; // phpcs:ignore } - - foreach ( $followers as $key => $follower ) { - if ( - \is_array( $follower ) && - isset( $follower['type'] ) && - 'Person' === $follower['type'] && - isset( $follower['id'] ) && - false !== \filter_var( $follower['id'], \FILTER_VALIDATE_URL ) - ) { - $followers[ $key ] = $follower['id']; - } - } - - return $followers; + return $items; } public static function count_followers( $author_id ) { - $followers = self::get_followers( $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::count_followers' ); - return \count( $followers ); + return \Activitypub\Collection\Followers::count_followers( $author_id ); } public static function add_follower( $actor, $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' ); - if ( ! \is_string( $actor ) ) { - if ( - \is_array( $actor ) && - isset( $actor['type'] ) && - 'Person' === $actor['type'] && - isset( $actor['id'] ) && - false !== \filter_var( $actor['id'], \FILTER_VALIDATE_URL ) - ) { - $actor = $actor['id']; - } - - return new \WP_Error( - 'invalid_actor_object', - \__( 'Unknown Actor schema', 'activitypub' ), - array( - 'status' => 404, - ) - ); - } - - if ( ! \is_array( $followers ) ) { - $followers = array( $actor ); - } else { - $followers[] = $actor; - } - - $followers = \array_unique( $followers ); - - \update_user_meta( $author_id, 'activitypub_followers', $followers ); + return \Activitypub\Collection\Followers::add_followers( $author_id, $actor ); } public static function remove_follower( $actor, $author_id ) { - $followers = \get_user_option( 'activitypub_followers', $author_id ); + _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::remove_follower' ); - foreach ( $followers as $key => $value ) { - if ( $value === $actor ) { - unset( $followers[ $key ] ); - } - } - - \update_user_meta( $author_id, 'activitypub_followers', $followers ); + return \Activitypub\Collection\Followers::remove_follower( $author_id, $actor ); } } From 734750b7968ad30b713e1d2ca8b43650eabdd194 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:41 +0200 Subject: [PATCH 047/427] use collection also for rest endpoints --- includes/rest/class-followers.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 3b1e146..ee7fe9d 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -1,6 +1,12 @@ \d+)/followers', array( array( - 'methods' => \WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::READABLE, 'callback' => array( self::class, 'get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', @@ -46,7 +52,7 @@ class Followers { $user = \get_user_by( 'ID', $user_id ); if ( ! $user ) { - return new \WP_Error( + return new WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( @@ -63,7 +69,7 @@ class Followers { */ \do_action( 'activitypub_outbox_pre' ); - $json = new \stdClass(); + $json = new stdClass(); $json->{'@context'} = \Activitypub\get_context(); @@ -73,14 +79,14 @@ class Followers { $json->type = 'OrderedCollectionPage'; $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore - $json->totalItems = \Activitypub\count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = \Activitypub\Peer\Followers::get_followers( $user_id ); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore + $json->orderedItems = array(); // phpcs:ignore + foreach ( FollowerCollection::get_followers( $user_id ) as $follower ) { + $json->orderedItems[] = $follower->name; // phpcs:ignore + } - $json->first = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); - - $response = new \WP_REST_Response( $json, 200 ); + $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); return $response; From 32194c31df392a3384e786e80ec9a7d1a6f2d26c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 15:57:49 +0200 Subject: [PATCH 048/427] phpDoc --- includes/collection/class-followers.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 7d7f8eb..e583b31 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -152,8 +152,10 @@ class Followers { } /** - * Undocumented function + * Add a new Follower * + * @param int $user_id The WordPress user + * @param string $actor The Actor URL * @return void */ public static function add_follower( $user_id, $actor ) { @@ -197,6 +199,11 @@ class Followers { wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); } + /** + * Undocumented function + * + * @return void + */ public static function send_ack() { // get inbox $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); @@ -212,6 +219,14 @@ class Followers { $response = safe_remote_post( $inbox, $activity, $user_id ); } + /** + * Get the Followers of a given user + * + * @param int $user_id + * @param int $number + * @param int $offset + * @return array The Term list of followers + */ public static function get_followers( $user_id, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); @@ -228,6 +243,12 @@ class Followers { return $terms->get_terms(); } + /** + * Count the total number of followers + * + * @param int $user_id The WordPress user + * @return int The number of Followers + */ public static function count_followers( $user_id ) { return count( self::get_followers( $user_id ) ); } From 3c86e94d9a79603aac2b809fbc54cb677c87928a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 16:25:15 +0200 Subject: [PATCH 049/427] remove followers --- includes/collection/class-followers.php | 6 +++++- includes/table/class-followers.php | 15 ++++++++++++--- templates/followers-list.php | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index e583b31..9848c5e 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -199,6 +199,10 @@ class Followers { wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); } + public static function remove_follower( $user_id, $actor ) { + wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + } + /** * Undocumented function * @@ -225,7 +229,7 @@ class Followers { * @param int $user_id * @param int $number * @param int $offset - * @return array The Term list of followers + * @return array The Term list of Followers */ public static function get_followers( $user_id, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index bba3208..dbd2647 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -59,8 +59,7 @@ class Followers extends WP_List_Table { public function get_bulk_actions() { return array( - 'revoke' => __( 'Revoke', 'activitypub' ), - 'verify' => __( 'Verify', 'activitypub' ), + 'delete' => __( 'Delete', 'activitypub' ), ); } @@ -79,6 +78,16 @@ class Followers extends WP_List_Table { } public function column_cb( $item ) { - return sprintf( '', esc_attr( $item['identifier'] ) ); + return sprintf( '', esc_attr( $item['identifier'] ) ); + } + + public function process_action() { + $followers = isset( $_REQUEST['followers'] ) ? $_REQUEST['followers'] : array(); // phpcs:ignore + + switch ( $this->current_action() ) { + case 'delete': + FollowerCollection::remove_follower( \get_current_user_id(), $followers ); + break; + } } } diff --git a/templates/followers-list.php b/templates/followers-list.php index 261808f..e76a45d 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -7,7 +7,7 @@ - + prepare_items(); $token_table->display(); From ebc9b6ac8d1479194a8234871734f3de21c863dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Apr 2023 16:34:47 +0200 Subject: [PATCH 050/427] naming improvements --- includes/class-admin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index fbb953a..c8c5813 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,7 +13,7 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); - \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) ); + \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } @@ -152,7 +152,7 @@ class Admin { // todo } - public static function add_fediverse_profile( $user ) { + public static function add_profile( $user ) { ?>

Date: Fri, 21 Apr 2023 16:40:46 +0200 Subject: [PATCH 051/427] verify requests --- includes/table/class-followers.php | 14 +++++++++++++- templates/followers-list.php | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index dbd2647..3cf4946 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -82,7 +82,19 @@ class Followers extends WP_List_Table { } public function process_action() { - $followers = isset( $_REQUEST['followers'] ) ? $_REQUEST['followers'] : array(); // phpcs:ignore + if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_apnonce'] ) ) { + return false; + } + + if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-followers-list' ) ) { + return false; + } + + if ( ! current_user_can( 'edit_user', \get_current_user_id() ) ) { + return false; + } + + $followers = $_REQUEST['followers']; // phpcs:ignore switch ( $this->current_action() ) { case 'delete': diff --git a/templates/followers-list.php b/templates/followers-list.php index e76a45d..c79c961 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -12,5 +12,6 @@ $token_table->prepare_items(); $token_table->display(); ?> +
From d23ff46073fecf307bba8c00ded5407048bc3674 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 08:45:10 -0600 Subject: [PATCH 052/427] fix merge omission --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index e00f9e0..77508c5 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -219,7 +219,7 @@ function get_publickey_by_actor( $actor, $key_id ) { return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); } -function get_follower_inboxes( $user_id ) { +function get_follower_inboxes( $user_id, $cc = array() ) { $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); $followers = array_merge( $followers, $cc ); $followers = array_unique( $followers ); From 1631f1c7dcc5cced91c4a3a87f53f28405ea9ae5 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 09:18:24 -0600 Subject: [PATCH 053/427] fix rest api endpoint --- includes/class-signature.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index f0ca349..0b2184d 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -112,12 +112,14 @@ class Signature { public static function verify_http_signature( $request ) { $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); if ( ! $headers ) { - $headers = self::default_server_headers(); + return false; } + + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + if ( array_key_exists( 'signature', $headers ) ) { $signature_block = self::parse_signature_header( $headers['signature'] ); } elseif ( array_key_exists( 'authorization', $headers ) ) { From 45ae73bb0660ed6062751ff1ca452e10e7a868eb Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 21 Apr 2023 17:20:48 +0200 Subject: [PATCH 054/427] Add Vary header --- includes/class-activitypub.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 4730515..b20eb71 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -39,6 +39,8 @@ class Activitypub { return $template; } + header( 'Vary: Accept' ); + // check if user can publish posts if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { return $template; From 7dbce74a96d2b149adbc53daf58c33ec7492b1e3 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 09:36:17 -0600 Subject: [PATCH 055/427] ensure signature block has algorithm --- includes/class-signature.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0b2184d..9261d99 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -184,11 +184,13 @@ class Signature { } public static function get_signature_algorithm( $signature_block ) { - switch ( $signature_block['algorithm'] ) { - case 'rsa-sha-512': - return 'sha512'; - default: - return 'sha256'; + 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 + default: + return 'sha256'; + } } return false; } From 4ed4d06fd5f651377551fa6860d2b5028cec00ca Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Fri, 21 Apr 2023 17:41:04 +0200 Subject: [PATCH 056/427] Add comment --- includes/class-activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index b20eb71..16a1f22 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -39,6 +39,7 @@ class Activitypub { return $template; } + // Ensure that edge caches know that this page can deliver both HTML and JSON. header( 'Vary: Accept' ); // check if user can publish posts From f396c6da4ed5dc3eaea51201a3b0ee564a7f7aeb Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 15:25:39 -0600 Subject: [PATCH 057/427] Optimize verification code and returns WP_Errors --- activitypub.php | 3 ++ includes/class-signature.php | 20 ++++++++----- includes/rest/class-inbox.php | 8 ----- includes/rest/class-server.php | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 includes/rest/class-server.php diff --git a/activitypub.php b/activitypub.php index 35e6de8..c5275ba 100644 --- a/activitypub.php +++ b/activitypub.php @@ -57,6 +57,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php'; Rest\Following::init(); + require_once \dirname( __FILE__ ) . '/includes/rest/class-server.php'; + \Activitypub\Rest\Server::init(); + require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php'; Rest\Webfinger::init(); diff --git a/includes/class-signature.php b/includes/class-signature.php index 9261d99..68fb41e 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -114,7 +114,7 @@ class Signature { $headers = $request->get_headers(); if ( ! $headers ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); } $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; @@ -127,7 +127,7 @@ class Signature { } if ( ! isset( $signature_block ) || ! $signature_block ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); } $signed_headers = $signature_block['headers']; @@ -137,12 +137,12 @@ class Signature { $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); if ( ! $signed_data ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); } $algorithm = self::get_signature_algorithm( $signature_block ); if ( ! $algorithm ) { - return false; + return new \WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { @@ -158,13 +158,17 @@ class Signature { } if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore - return false; + return new \WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); } } - $public_key = \rtrim( \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ) ); // phpcs:ignore - - return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + $public_key = \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ); // phpcs:ignore + if ( \is_wp_error( $public_key ) ) { + return $public_key; + } else { + $public_key = \rtrim( $public_key ); + } + return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0 ?? new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 8a1f139..0257ba1 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -134,10 +134,6 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { - // SecureMode/Authorized fetch. - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return new \WP_REST_Response( array(), 403 ); - } $user_id = $request->get_param( 'user_id' ); @@ -159,10 +155,6 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { - // SecureMode/Authorized fetch. - if ( ! \Activitypub\Signature::verify_http_signature( $request ) ) { - return new \WP_REST_Response( array(), 403 ); - } $data = $request->get_params(); $type = $request->get_param( 'type' ); diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php new file mode 100644 index 0000000..790728a --- /dev/null +++ b/includes/rest/class-server.php @@ -0,0 +1,55 @@ +get_route(); + if ( str_starts_with( $maybe_activitypub, '/activitypub' ) ) { + if ( 'POST' === $request->get_method() ) { + $verified_request = Signature::verify_http_signature( $request ); + + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } else { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + + if ( $secure_mode ) { + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } + } + } + } +} From 023ba25f38aa08447fdf9eed8a84cb721618bd9d Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 15:27:02 -0600 Subject: [PATCH 058/427] PHPDoc --- includes/class-signature.php | 45 ++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 68fb41e..a889967 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -110,6 +110,13 @@ class Signature { } } + /** + * Verifies the http signatures + * + * @param WP_REQUEST | Array $request + * @return void + * @author Django Doucet + */ public static function verify_http_signature( $request ) { $headers = $request->get_headers(); @@ -172,21 +179,13 @@ class Signature { } - public static function default_server_headers() { - $headers = array( - '(request-target)' => strtolower( $_SERVER['REQUEST_METHOD'] ) . ' ' . $_SERVER['REQUEST_URI'], - 'content-type' => $_SERVER['CONTENT_TYPE'], - 'content-length' => $_SERVER['CONTENT_LENGTH'], - ); - foreach ( $_SERVER as $k => $v ) { - if ( \strpos( $k, 'HTTP_' ) === 0 ) { - $field = \str_replace( '_', '-', \strtolower( \substr( $k, 5 ) ) ); - $headers[ $field ] = $v; - } - } - return $headers; - } - + /** + * Gets the signature algorithm from the signature header + * + * @param array $signature_block + * @return string algorithm + * @author Django Doucet + */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { switch ( $signature_block['algorithm'] ) { @@ -199,6 +198,13 @@ class Signature { return false; } + /** + * Parses the Signature header + * + * @param array $header + * @return array signature parts + * @author Django Doucet + */ public static function parse_signature_header( $header ) { $ret = array(); $matches = array(); @@ -230,6 +236,15 @@ class Signature { return $ret; } + /** + * Gets the header data from the included pseudo headers + * + * @param array $signed_headers + * @param array $signature_block (pseudo-headers) + * @param array $headers (original http headers) + * @return signed headers for comparison + * @author Django Doucet + */ 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. From b641cb03f36eed34c86f3bdde3569fdb9378224b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 21 Apr 2023 16:16:52 -0600 Subject: [PATCH 059/427] fix phpcs --- includes/class-signature.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a889967..608bff3 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -175,7 +175,10 @@ class Signature { } else { $public_key = \rtrim( $public_key ); } - return \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0 ?? new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + if ( ! $verified ) { + return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); // phpcs:ignore null coalescing operator + } } From 28c077e422645d84ea4fc8ff41157e5555fe59a2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 23 Apr 2023 22:56:45 +0200 Subject: [PATCH 060/427] Add URL --- includes/model/class-post.php | 43 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index cfe49b6..76d4d03 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -28,6 +28,13 @@ class Post { */ private $id; + /** + * The Object URL. + * + * @var string + */ + private $url; + /** * The Object Summary. * @@ -179,6 +186,7 @@ class Post { $array = array( 'id' => $this->get_id(), + 'url' => $this->get_url(), 'type' => $this->get_object_type(), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), 'attributedTo' => \get_author_posts_url( $post->post_author ), @@ -206,6 +214,29 @@ class Post { return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } + /** + * Returns the URL of an Activity Object + * + * @return string + */ + public function get_url() { + if ( $this->url ) { + return $this->url; + } + + $post = $this->post; + + if ( 'trash' === get_post_status( $post ) ) { + $permalink = \get_post_meta( $post->url, 'activitypub_canonical_url', true ); + } else { + $permalink = \get_permalink( $post ); + } + + $this->url = $permalink; + + return $permalink; + } + /** * Returns the ID of an Activity Object * @@ -216,17 +247,9 @@ class Post { return $this->id; } - $post = $this->post; + $this->id = $this->get_url(); - if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); - } else { - $permalink = \get_permalink( $post ); - } - - $this->id = $permalink; - - return $permalink; + return $this->id; } /** From 77415ef510a1ebea33d567c354e87e02cf0df824 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Sun, 23 Apr 2023 22:57:03 +0200 Subject: [PATCH 061/427] Remove "(Fediverse)" --- includes/class-admin.php | 2 +- templates/followers-list.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 220ed9b..fbb953a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -31,7 +31,7 @@ class Admin { \add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) ); - $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); + $followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) ); \add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) ); } diff --git a/templates/followers-list.php b/templates/followers-list.php index 057f498..a7136ce 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,5 +1,5 @@
-

+

From 47dc2f72d1095eb02fe0975c95936c82a139ddc5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Apr 2023 09:49:06 +0200 Subject: [PATCH 062/427] fix "bulk replace" issue --- includes/model/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 76d4d03..470ad70 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -227,7 +227,7 @@ class Post { $post = $this->post; if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->url, 'activitypub_canonical_url', true ); + $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); } else { $permalink = \get_permalink( $post ); } From 3f4c44db05e7a6b8df24f7891a177b865972e4d8 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 24 Apr 2023 09:15:48 +0200 Subject: [PATCH 063/427] Compatibility: do not serve images with Jetpack CDN when active When Jetpack's image CDN is active, core calls to retrieve images return an image served by the CDN. Since Fediverse instances usually fetch and cache the data themselves, we do not need to use the CDN for those images when returned by the ActivityPub plugin. In fact, we really do not want that to happen, as Fediverse instances may get errors when trying to fetch images from the CDN (they may get blocked / rate-limited / ...). Let's hook into Jetpack's CDN to avoid that. --- includes/model/class-post.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 470ad70..6f33cc3 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -306,7 +306,24 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + + /** + * If you use the Jetpack plugin and its Image CDN, aka Photon, + * the image strings returned will use the Photon URL. + * We don't want that since Fediverse instances already do caching on their end. + * Let the CDN only be used for visitors of the site. + */ + if ( class_exists( 'Jetpack_Photon' ) ) { + \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); + } + $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); + + // Re-enable Photon now that the image URL has been built. + if ( class_exists( 'Jetpack_Photon' ) ) { + \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); + } + $mimetype = \get_post_mime_type( $id ); if ( $thumbnail ) { From 56d2b7e8be6b6765138dbd6a4170a056c7e95794 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 24 Apr 2023 09:49:05 +0200 Subject: [PATCH 064/427] Update to handle both old and new versions of Jetpack See https://github.com/Automattic/jetpack/pull/30050/ --- includes/model/class-post.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 6f33cc3..a69e139 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -312,15 +312,23 @@ class Post { * the image strings returned will use the Photon URL. * We don't want that since Fediverse instances already do caching on their end. * Let the CDN only be used for visitors of the site. + * + * Old versions of Jetpack used the Jetpack_Photon class to do this. + * New versions use the Image_CDN class. + * Let's handle both. */ - if ( class_exists( 'Jetpack_Photon' ) ) { + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); } $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); // Re-enable Photon now that the image URL has been built. - if ( class_exists( 'Jetpack_Photon' ) ) { + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); } From 84a82c2ac41a44bb4278d46072b193193c18a1c8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Apr 2023 20:46:51 +0200 Subject: [PATCH 065/427] added follower model --- activitypub.php | 3 + includes/class-activity-dispatcher.php | 7 +- includes/collection/class-followers.php | 181 ++++++++++++++--------- includes/functions.php | 96 +----------- includes/model/class-follower.php | 187 ++++++++++++++++++++++++ includes/peer/class-followers.php | 6 +- includes/rest/class-followers.php | 5 +- includes/rest/class-inbox.php | 40 +---- includes/table/class-followers.php | 10 +- 9 files changed, 320 insertions(+), 215 deletions(-) create mode 100644 includes/model/class-follower.php diff --git a/activitypub.php b/activitypub.php index c1cb823..6439ec8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,6 +29,8 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); + \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); + require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; @@ -37,6 +39,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; + require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; Activity_Dispatcher::init(); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 57fee1a..73e49b5 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use Activitypub\Collection\Followers; /** * ActivityPub Activity_Dispatcher Class @@ -66,11 +67,9 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = \Activitypub\get_follower_inboxes( $user_id, $activitypub_activity->get_cc() ); + $inboxes = FollowerCollection::get_inboxes( $user_id ); - foreach ( $inboxes as $inbox => $cc ) { - $cc = array_values( array_unique( $cc ) ); - $activitypub_activity->add_cc( $cc ); + foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 9848c5e..5964756 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -5,10 +5,10 @@ use WP_Error; use WP_Term_Query; use Activitypub\Webfinger; use Activitypub\Model\Activity; +use Activitypub\Model\Follower; use function Activitypub\safe_remote_get; use function Activitypub\safe_remote_post; -use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection @@ -29,6 +29,8 @@ class Followers { \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 ); + + \add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 ); } /** @@ -59,16 +61,6 @@ class Followers { register_taxonomy( self::TAXONOMY, 'user', $args ); register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); - register_term_meta( - self::TAXONOMY, - 'user_id', - array( - 'type' => 'string', - 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_username' ), - ) - ); - register_term_meta( self::TAXONOMY, 'name', @@ -101,21 +93,21 @@ class Followers { register_term_meta( self::TAXONOMY, - 'created_at', + 'inbox', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_inbox' ), ) ); register_term_meta( self::TAXONOMY, - 'inbox', + 'updated_at', array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_created_at' ), + //'sanitize_callback' => array( self::class, 'validate_updated_at' ), ) ); @@ -127,12 +119,13 @@ class Followers { * * @param array $object The JSON "Follow" Activity * @param int $user_id The ID of the WordPress User - * * @return void */ public static function handle_follow_request( $object, $user_id ) { // save follower - self::add_follower( $user_id, $object['actor'] ); + $follower = self::add_follower( $user_id, $object['actor'] ); + + do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower ); } /** @@ -156,68 +149,67 @@ class Followers { * * @param int $user_id The WordPress user * @param string $actor The Actor URL - * @return void + * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { - $remote_data = get_remote_metadata_by_actor( $actor ); + $follower = new Follower( $actor ); + $follower->upsert(); - if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { - $remote_data = array(); + $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); + + if ( is_wp_error( $result ) ) { + return $result; + } else { + return $follower; } - - $term = term_exists( $actor, self::TAXONOMY ); - - if ( ! $term ) { - $term = wp_insert_term( - $actor, - self::TAXONOMY, - array( - 'slug' => sanitize_title( $actor ), - 'description' => wp_json_encode( $remote_data ), - ) - ); - } - - $term_id = $term['term_id']; - - $map_meta = array( - 'name' => 'name', - 'preferredUsername' => 'username', - 'inbox' => 'inbox', - ); - - foreach ( $map_meta as $remote => $internal ) { - if ( ! empty( $remote_data[ $remote ] ) ) { - update_term_meta( $term_id, $internal, esc_html( $remote_data[ $remote ] ), true ); - } - } - - if ( ! empty( $remote_data['icon']['url'] ) ) { - update_term_meta( $term_id, 'avatar', esc_url_raw( $remote_data['icon']['url'] ), true ); - } - - wp_set_object_terms( $user_id, $actor, self::TAXONOMY, true ); - } - - public static function remove_follower( $user_id, $actor ) { - wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); } /** - * Undocumented function + * Remove a Follower * + * @param int $user_id The WordPress user_id + * @param string $actor The Actor URL + * @return bool|WP_Error True on success, false or WP_Error on failure. + */ + public static function remove_follower( $user_id, $actor ) { + return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + } + + /** + * Remove a Follower + * + * @param string $actor The Actor URL + * @return \Activitypub\Model\Follower The Follower object + */ + public static function get_follower( $actor ) { + $term = get_term_by( 'name', $actor, self::TAXONOMY ); + + return new Follower( $term->name ); + } + + /** + * Send Accept response + * + * @param string $actor The Actor URL + * @param array $object The Activity object + * @param int $user_id The WordPress user_id + * @param Activitypub\Model\Follower $follower The Follower object * @return void */ - public static function send_ack() { + public static function send_follow_response( $actor, $object, $user_id, $follower ) { + //if ( is_wp_error( $follower ) ) { + // @todo send error message + //} + // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); + $inbox = $follower->get_inbox(); // send "Accept" activity $activity = new Activity( 'Accept' ); $activity->set_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); + $activity->set_to( $actor ); + $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity = $activity->to_simple_json(); $response = safe_remote_post( $inbox, $activity, $user_id ); @@ -226,12 +218,13 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id - * @param int $number - * @param int $offset - * @return array The Term list of Followers + * @param int $user_id The WordPress user_id + * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param int $number Limts the result + * @param int $offset Offset + * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $offset = null ) { + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); $terms = new WP_Term_Query( @@ -244,7 +237,24 @@ class Followers { ) ); - return $terms->get_terms(); + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } } /** @@ -257,6 +267,39 @@ class Followers { return count( self::get_followers( $user_id ) ); } + public static function get_inboxes( $user_id ) { + // get all Followers of a WordPress user + $terms = new WP_Term_Query( + array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'fields' => 'ids', + ) + ); + + $terms = $terms->get_terms(); + + global $wpdb; + $results = $wpdb->get_col( + $wpdb->prepare( + "SELECT DISTINCT meta_value FROM {$wpdb->termmeta} + WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ") + AND meta_key = 'shared_inbox' + AND meta_value IS NOT NULL", + $terms + ) + ); + + return array_filter( $results ); + } + + /** + * Undocumented function + * + * @param [type] $user_id + * @return void + */ public static function migrate_followers( $user_id ) { $followes = get_user_meta( $user_id, 'activitypub_followers', true ); diff --git a/includes/functions.php b/includes/functions.php index 77508c5..74b0c2e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -63,7 +63,7 @@ function safe_remote_post( $url, $body, $user_id ) { function safe_remote_get( $url, $user_id ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date ); + $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); @@ -95,15 +95,14 @@ function safe_remote_get( $url, $user_id ) { * @return string The user-resource */ function get_webfinger_resource( $user_id ) { - return \Activitypub\Webfinger::get_user_resource( $user_id ); + return Webfinger::get_user_resource( $user_id ); } /** - * [get_metadata_by_actor description] + * Requests the Meta-Data from the Actors profile * - * @param string $actor - * - * @return array + * @param string $actor The Actor URL + * @return array The Actor profile as array */ function get_remote_metadata_by_actor( $actor ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); @@ -165,82 +164,9 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - return $metadata; } -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_inbox_by_actor( $actor ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) { - return $metadata['endpoints']['sharedInbox']; - } - - if ( \array_key_exists( 'inbox', $metadata ) ) { - return $metadata['inbox']; - } - - return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); -} - -/** - * [get_inbox_by_actor description] - * @param [type] $actor [description] - * @return [type] [description] - */ -function get_publickey_by_actor( $actor, $key_id ) { - $metadata = \Activitypub\get_remote_metadata_by_actor( $actor ); - - if ( \is_wp_error( $metadata ) ) { - return $metadata; - } - - if ( - isset( $metadata['publicKey'] ) && - isset( $metadata['publicKey']['id'] ) && - isset( $metadata['publicKey']['owner'] ) && - isset( $metadata['publicKey']['publicKeyPem'] ) && - $key_id === $metadata['publicKey']['id'] && - $actor === $metadata['publicKey']['owner'] - ) { - return $metadata['publicKey']['publicKeyPem']; - } - - return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata ); -} - -function get_follower_inboxes( $user_id, $cc = array() ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - $followers = array_merge( $followers, $cc ); - $followers = array_unique( $followers ); - - $inboxes = array(); - - foreach ( $followers as $follower ) { - $inbox = \Activitypub\get_inbox_by_actor( $follower ); - if ( ! $inbox || \is_wp_error( $inbox ) ) { - continue; - } - // init array if empty - if ( ! isset( $inboxes[ $inbox ] ) ) { - $inboxes[ $inbox ] = array(); - } - $inboxes[ $inbox ][] = $follower; - } - - return $inboxes; -} - function get_identifier_settings( $user_id ) { ?> @@ -261,19 +187,11 @@ function get_identifier_settings( $user_id ) { } function get_followers( $user_id ) { - $followers = \Activitypub\Peer\Followers::get_followers( $user_id ); - - if ( ! $followers ) { - return array(); - } - - return $followers; + return Collection\Followers::get_followers( $user_id ); } function count_followers( $user_id ) { - $followers = \Activitypub\get_followers( $user_id ); - - return \count( $followers ); + return Collection\Followers::count_followers( $user_id ); } /** diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php new file mode 100644 index 0000000..169e5f2 --- /dev/null +++ b/includes/model/class-follower.php @@ -0,0 +1,187 @@ + 'name', + 'preferredUsername' => 'username', + 'inbox' => 'inbox', + ); + + /** + * Constructor + * + * @param WP_Post $post + */ + public function __construct( $actor ) { + $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); + + $this->actor = $actor; + + if ( $term ) { + $this->id = $term->term_id; + $this->slug = $term->slug; + $this->meta = json_decode( $term->meta ); + } else { + $this->slug = sanitize_title( $actor ); + } + } + + /** + * Magic function to implement getter and setter + * + * @param string $method + * @param string $params + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( empty( $this->$var ) ) { + return $this->get( $var ); + } + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->$var = $params[0]; + } + } + + public function get( $attribute ) { + if ( $this->$attribute ) { + return $this->$attribute; + } + + if ( ! $this->id ) { + $this->$attribute = $this->get_meta_by( $attribute ); + return $this->$attribute; + } + + $this->$attribute = get_term_meta( $this->id, $attribute, true ); + return $this->$attribute; + } + + public function get_meta_by( $attribute, $force = false ) { + $meta = $this->get_meta( $force ); + + foreach ( $this->map_meta as $remote => $local ) { + if ( $attribute === $local && isset( $meta[ $remote ] ) ) { + return $meta[ $remote ]; + } + } + + return null; + } + + public function get_meta( $force = false ) { + if ( $this->meta && false === (bool) $force ) { + return $this->meta; + } + + $remote_data = get_remote_metadata_by_actor( $this->actor ); + + if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { + $remote_data = array(); + } + + $this->meta = $remote_data; + + return $this->meta; + } + + public function update() { + $term = wp_update_term( + $this->id, + Followers::TAXONOMY, + array( + 'description' => wp_json_encode( $this->get_meta( true ) ), + ) + ); + + $this->update_term_meta(); + } + + public function save() { + $term = wp_insert_term( + $this->actor, + Followers::TAXONOMY, + array( + 'slug' => sanitize_title( $this->get_actor() ), + 'description' => wp_json_encode( $this->get_meta() ), + ) + ); + + $this->id = $term['term_id']; + + $this->update_term_meta(); + } + + public function upsert() { + if ( $this->id ) { + $this->update(); + } else { + $this->save(); + } + } + + protected function update_term_meta() { + $meta = $this->get_meta(); + + foreach ( $this->map_meta as $remote => $internal ) { + if ( ! empty( $meta[ $remote ] ) ) { + update_term_meta( $this->id, $internal, esc_html( $meta[ $remote ] ), true ); + $this->$internal = $meta[ $remote ]; + } + } + + if ( ! empty( $meta['icon']['url'] ) ) { + update_term_meta( $this->id, 'avatar', esc_url_raw( $meta['icon']['url'] ), true ); + $this->avatar = $meta['icon']['url']; + } + + if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['endpoints']['sharedInbox'] ), true ); + $this->shared_inbox = $meta['endpoints']['sharedInbox']; + } elseif ( ! empty( $meta['inbox'] ) ) { + update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['inbox'] ), true ); + $this->shared_inbox = $meta['inbox']; + } + + update_term_meta( $this->id, 'updated_at', \strtotime( 'now' ), true ); + } +} diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index d5caf10..8941cc4 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -11,11 +11,7 @@ class Followers { public static function get_followers( $author_id ) { _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' ); - $items = array(); // phpcs:ignore - foreach ( \Activitypub\Collection\Followers::get_followers( $author_id ) as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + return \Activitypub\Collection\Followers::get_followers( $author_id ); } public static function count_followers( $author_id ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index ee7fe9d..9a8b9b6 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -81,10 +81,7 @@ class Followers { $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = array(); // phpcs:ignore - foreach ( FollowerCollection::get_followers( $user_id ) as $follower ) { - $json->orderedItems[] = $follower->name; // phpcs:ignore - } + $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 761bfd6..5a23d02 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -17,8 +17,7 @@ class Inbox { public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); - //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); - //\add_action( 'activitypub_inbox_announce', array( self::class, 'handle_reaction' ), 10, 2 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); } @@ -342,43 +341,6 @@ class Inbox { return $params; } - /** - * Handles "Follow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_follow( $object, $user_id ) { - // save follower - \Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id ); - - // get inbox - $inbox = \Activitypub\get_inbox_by_actor( $object['actor'] ); - - // send "Accept" activity - $activity = new Activity( 'Accept' ); - $activity->set_object( $object ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); - $activity->set_to( $object['actor'] ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) ); - - $activity = $activity->to_simple_json(); - - $response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); - } - - /** - * Handles "Unfollow" requests - * - * @param array $object The activity-object - * @param int $user_id The id of the local blog-user - */ - public static function handle_unfollow( $object, $user_id ) { - if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) { - \Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id ); - } - } - /** * Handles "Reaction" requests * diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 3cf4946..13d6a91 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -33,7 +33,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page ); $counter = FollowerCollection::count_followers( \get_current_user_id() ); $this->items = array(); @@ -47,10 +47,10 @@ class Followers extends WP_List_Table { foreach ( $follower as $follower ) { $item = array( - 'avatar' => esc_attr( get_term_meta( $follower->term_id, 'avatar', true ) ), - 'name' => esc_attr( get_term_meta( $follower->term_id, 'name', true ) ), - 'username' => esc_attr( get_term_meta( $follower->term_id, 'username', true ) ), - 'identifier' => esc_attr( $follower->name ), + 'avatar' => esc_attr( $follower->get_avatar() ), + 'name' => esc_attr( $follower->get_name() ), + 'username' => esc_attr( $follower->get_username() ), + 'identifier' => esc_attr( $follower->get_actor() ), ); $this->items[] = $item; From 377fc9416198b371d53ab6d231afbf170853d528 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 09:09:07 +0200 Subject: [PATCH 066/427] php doc --- includes/collection/class-followers.php | 52 ++++++++++++------- includes/model/class-follower.php | 67 ++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 5964756..78d0139 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -118,7 +118,8 @@ class Followers { * Handle the "Follow" Request * * @param array $object The JSON "Follow" Activity - * @param int $user_id The ID of the WordPress User + * @param int $user_id The ID of the ID of the WordPress User + * * @return void */ public static function handle_follow_request( $object, $user_id ) { @@ -131,8 +132,8 @@ class Followers { /** * Handles "Unfollow" requests * - * @param array $object The JSON "Undo" Activity - * @param int $user_id The ID of the WordPress User + * @param array $object The JSON "Undo" Activity + * @param int $user_id The ID of the ID of the WordPress User */ public static function handle_undo_request( $object, $user_id ) { if ( @@ -147,8 +148,9 @@ class Followers { /** * Add a new Follower * - * @param int $user_id The WordPress user + * @param int $user_id The ID of the WordPress User * @param string $actor The Actor URL + * * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { @@ -167,9 +169,10 @@ class Followers { /** * Remove a Follower * - * @param int $user_id The WordPress user_id - * @param string $actor The Actor URL - * @return bool|WP_Error True on success, false or WP_Error on failure. + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL + * + * @return bool|WP_Error True on success, false or WP_Error on failure. */ public static function remove_follower( $user_id, $actor ) { return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); @@ -178,7 +181,8 @@ class Followers { /** * Remove a Follower * - * @param string $actor The Actor URL + * @param string $actor The Actor URL + * * @return \Activitypub\Model\Follower The Follower object */ public static function get_follower( $actor ) { @@ -192,8 +196,9 @@ class Followers { * * @param string $actor The Actor URL * @param array $object The Activity object - * @param int $user_id The WordPress user_id + * @param int $user_id The ID of the WordPress User * @param Activitypub\Model\Follower $follower The Follower object + * * @return void */ public static function send_follow_response( $actor, $object, $user_id, $follower ) { @@ -218,11 +223,12 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id The WordPress user_id - * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT - * @param int $number Limts the result - * @param int $offset Offset - * @return array The Term list of Followers, the format depends on $output + * @param int $user_id The ID of the WordPress User + * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param int $number Limts the result + * @param int $offset Offset + * + * @return array The Term list of Followers, the format depends on $output */ public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { //self::migrate_followers( $user_id ); @@ -260,15 +266,23 @@ class Followers { /** * Count the total number of followers * - * @param int $user_id The WordPress user - * @return int The number of Followers + * @param int $user_id The ID of the WordPress User + * + * @return int The number of Followers */ public static function count_followers( $user_id ) { return count( self::get_followers( $user_id ) ); } + /** + * Returns all Inboxes fo a Users Followers + * + * @param int $user_id The ID of the WordPress User + * + * @return array The list of Inboxes + */ public static function get_inboxes( $user_id ) { - // get all Followers of a WordPress user + // get all Followers of a ID of the WordPress User $terms = new WP_Term_Query( array( 'taxonomy' => self::TAXONOMY, @@ -295,9 +309,9 @@ class Followers { } /** - * Undocumented function + * Migrate Followers * - * @param [type] $user_id + * @param int $user_id The ID of the WordPress User * @return void */ public static function migrate_followers( $user_id ) { diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 169e5f2..84c6b0a 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -8,32 +8,97 @@ use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Follower Class * + * This Object represents a single Follower. + * There is no direct reference to a WordPress User here. + * * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox */ class Follower { - + /** + * The Object ID + * + * @var int + */ private $id; + /** + * The Actor-URL of the Follower + * + * @var string + */ private $actor; + /** + * The Object slug + * + * This is a requirement of the Term-Meta but will not + * be actively used in the ActivityPub context. + * + * @var string + */ private $slug; + /** + * The Object Name + * + * This is the same as the Actor-URL + * + * @var string + */ private $name; + /** + * The Username + * + * @var string + */ private $username; + /** + * The Avatar URL + * + * @var string + */ private $avatar; + /** + * The URL to the Followers Inbox + * + * @var string + */ private $inbox; + /** + * The URL to the Servers Shared-Inbox + * + * If the Server does not support Shared-Inboxes, + * the Inbox will be stored. + * + * @var string + */ private $shared_inbox; + /** + * The date, the Follower was updated + * + * @var string untixtimestamp + */ private $updated_at; + /** + * The complete Remote-Profile of the Follower + * + * @var array + */ private $meta; + /** + * Maps the meta fields to the local db fields + * + * @var array + */ private $map_meta = array( 'name' => 'name', 'preferredUsername' => 'username', From 764a09104679bddf5b9b8e07141f349d3b2a6eec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 09:31:28 +0200 Subject: [PATCH 067/427] fix unit tests --- includes/class-activity-dispatcher.php | 2 +- includes/collection/class-followers.php | 6 ++ includes/peer/class-followers.php | 2 +- ...-class-activitypub-activity-dispatcher.php | 7 +- tests/test-class-db-activitypub-followers.php | 78 ++++++++++++++++--- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 73e49b5..1e31616 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -67,7 +67,7 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = FollowerCollection::get_inboxes( $user_id ); + $inboxes = Followers::get_inboxes( $user_id ); foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 78d0139..9669398 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -240,6 +240,8 @@ class Followers { 'object_ids' => $user_id, 'number' => $number, 'offset' => $offset, + 'orderby' => 'id', + 'order' => 'ASC', ) ); @@ -294,6 +296,10 @@ class Followers { $terms = $terms->get_terms(); + if ( ! $terms ) { + return array(); + } + global $wpdb; $results = $wpdb->get_col( $wpdb->prepare( diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php index 8941cc4..e0e6ddb 100644 --- a/includes/peer/class-followers.php +++ b/includes/peer/class-followers.php @@ -23,7 +23,7 @@ class Followers { public static function add_follower( $actor, $author_id ) { _deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' ); - return \Activitypub\Collection\Followers::add_followers( $author_id, $actor ); + return \Activitypub\Collection\Followers::add_follower( $author_id, $actor ); } public static function remove_follower( $actor, $author_id ) { diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 0e503e5..bde52ab 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -5,17 +5,22 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', + 'prefferedUsername' => 'username', ), 'jon@example.com' => array( 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', + 'prefferedUsername' => 'jon', ), ); public function test_dispatch_activity() { $followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' ); - \update_user_meta( 1, 'activitypub_followers', $followers ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } $post = \wp_insert_post( array( diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index c502bf2..0591178 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -1,15 +1,37 @@ 'Person', - 'id' => 'http://sally.example.org', - 'name' => 'Sally Smith', - ); - \update_user_meta( 1, 'activitypub_followers', $followers ); + public static $users = array( + 'username@example.org' => array( + 'url' => 'https://example.org/users/username', + 'inbox' => 'https://example.org/users/username/inbox', + 'name' => 'username', + 'prefferedUsername' => 'username', + ), + 'jon@example.com' => array( + 'url' => 'https://example.com/author/jon', + 'inbox' => 'https://example.com/author/jon/inbox', + 'name' => 'jon', + 'prefferedUsername' => 'jon', + ), + 'sally@example.org' => array( + 'url' => 'http://sally.example.org', + 'inbox' => 'http://sally.example.org/inbox', + 'name' => 'jon', + 'prefferedUsername' => 'jon', + ), + ); - $db_followers = \Activitypub\Peer\Followers::get_followers( 1 ); + public function test_get_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 3, \count( $db_followers ) ); @@ -17,11 +39,43 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } public function test_add_follower() { - $follower = 'https://example.com/author/' . \time(); - \Activitypub\Peer\Followers::add_follower( $follower, 1 ); + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $db_followers = \Activitypub\Peer\Followers::get_followers( 1 ); + $follower = 'https://example.com/author/' . \time(); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertContains( $follower, $db_followers ); } + + public static function http_request_host_is_external( $in, $host ) { + if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { + return true; + } + return $in; + } + public static function http_request_args( $args, $url ) { + if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) { + $args['reject_unsafe_urls'] = false; + } + return $args; + } + + public static function pre_http_request( $preempt, $request, $url ) { + return array( + 'headers' => array( + 'content-type' => 'text/json', + ), + 'body' => '', + 'response' => array( + 'code' => 202, + ), + ); + } + + public static function http_response( $response, $args, $url ) { + return $response; + } } From da63763ddcba83d3df488c62d9693cf4b100e18e Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 10:54:21 +0200 Subject: [PATCH 068/427] Compat: only disable Jetpack's image CDN via filter This follows the discussion in #307. 1. Do not disable Jetpack's image CDN in ActivityPub requests by default. 2. Add a new filter, activitypub_images_use_jetpack_image_cdn, that site owners can use to disable Jetpack's Image CDN if they'd like to. 3. Extract image getting into its own method for improved readability. --- includes/model/class-post.php | 87 ++++++++++++++++++++++------------- 1 file changed, 55 insertions(+), 32 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index a69e139..254fb8c 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -305,41 +305,16 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { - $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); - - /** - * If you use the Jetpack plugin and its Image CDN, aka Photon, - * the image strings returned will use the Photon URL. - * We don't want that since Fediverse instances already do caching on their end. - * Let the CDN only be used for visitors of the site. - * - * Old versions of Jetpack used the Jetpack_Photon class to do this. - * New versions use the Image_CDN class. - * Let's handle both. - */ - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); - } - - $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); - - // Re-enable Photon now that the image URL has been built. - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); - } - - $mimetype = \get_post_mime_type( $id ); - + $thumbnail = $this->get_image( $id ); if ( $thumbnail ) { - $image = array( - 'type' => 'Image', - 'url' => $thumbnail[0], + $mimetype = \get_post_mime_type( $id ); + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + $image = array( + 'type' => 'Image', + 'url' => $thumbnail[0], 'mediaType' => $mimetype, ); + if ( $alt ) { $image['name'] = $alt; } @@ -352,6 +327,54 @@ class Post { return $images; } + /** + * Return details about an image attachment. + * + * Can return a CDNized URL if Jetpack's image CDN is active. + * This can be disabled with a filter. + * + * @param int $id The attachment ID. + * + * @return array|false Array of image data, or boolean false if no image is available. + */ + public function get_image( $id ) { + /** + * Allow bypassing Jetpack's Image CDN when returning image URLs. + * + * @param bool $should_use_cdn Whether to use the Jetpack Image CDN. True by default. + */ + $should_use_cdn = apply_filters( 'activitypub_images_use_jetpack_image_cdn', true ); + + if ( $should_use_cdn ) { + // Return the full URL, using a CDN URL if Jetpack's image CDN is active. + return \wp_get_attachment_image_src( $id, 'full' ); + } + + /* + * Disable Jetpacks image CDN image processing for this request. + * + * Note: old versions of Jetpack used the Jetpack_Photon class to do this. + * New versions use the Image_CDN class. + * Let's handle both. + */ + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { + \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); + } + + $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); + + // Re-enable Photon now that the image URL has been built. + if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { + \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); + } elseif ( \class_exists( 'Jetpack_Photon' ) ) { + \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); + } + + return $thumbnail; + } + /** * Returns a list of Tags, used in the Post * From 3fa4a7b58ed3520d567d1d868c8e690db73e271a Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 10:56:17 +0200 Subject: [PATCH 069/427] Add readme entry --- readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.txt b/readme.txt index 9c7661d..b73ec40 100644 --- a/readme.txt +++ b/readme.txt @@ -113,6 +113,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += Next = + +* Compatibility: add filter to allow disabling Jetpack's image CDN when returning images in ActivityPub requests. + = 0.17.0 = * Fix type-selector From 2afe74b29bf49e5d3745a5f21f10db09892c3001 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 11:03:33 +0200 Subject: [PATCH 070/427] Compatibility: the plugin is compatible with WP 6.2. --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 9c7661d..6fe6189 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: pfefferle, mediaformat, akirk, automattic Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 -Tested up to: 6.1 +Tested up to: 6.2 Stable tag: 0.17.0 Requires PHP: 5.6 License: MIT From 6ee59d2a10e5e1d53b3cc93b30850d98a1403145 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Tue, 25 Apr 2023 11:04:32 +0200 Subject: [PATCH 071/427] Add changelog --- readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/readme.txt b/readme.txt index 6fe6189..e89c768 100644 --- a/readme.txt +++ b/readme.txt @@ -113,6 +113,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). += Next = + +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. + = 0.17.0 = * Fix type-selector From d1f6973d9b02869c9e59e7cc4221265d3e5c22b9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Apr 2023 11:59:08 +0200 Subject: [PATCH 072/427] re-add mention functionality not perfect but works as expected --- includes/class-activity-dispatcher.php | 6 ++- includes/class-mention.php | 54 +++++++++++++++++++++++++- includes/functions.php | 13 +++++-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 1e31616..e71ac19 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -67,7 +67,11 @@ class Activity_Dispatcher { $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); diff --git a/includes/class-mention.php b/includes/class-mention.php index e0930cc..7167d14 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -1,6 +1,8 @@ Date: Tue, 25 Apr 2023 20:44:54 +0200 Subject: [PATCH 073/427] ignore `www` subdomain when comparing hosts fix #290 --- includes/rest/class-webfinger.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index d41cf25..10dcfa4 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -54,15 +54,16 @@ class Webfinger { $resource = \str_replace( 'acct:', '', $resource ); $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \substr( \strrchr( $resource, '@' ), 1 ); + $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); - if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) { + if ( $blog_host !== $resource_host ) { return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); } $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); - if ( ! $user || ! user_can( $user, 'publish_posts' ) ) { + if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); } From 98619dc3198181ad44736afdb82cf74ae31a296d Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Wed, 26 Apr 2023 10:08:22 +0200 Subject: [PATCH 074/427] Protect img tags from replacing mentions --- includes/class-mention.php | 6 ++++++ tests/test-class-activitypub-mention.php | 1 + 2 files changed, 7 insertions(+) diff --git a/includes/class-mention.php b/includes/class-mention.php index e0930cc..9c93d80 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -46,6 +46,12 @@ class Mention { $the_content ); + $the_content = preg_replace_callback( + '#]+>#i', + $protect, + $the_content + ); + $the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $the_content ); $the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content ); diff --git a/tests/test-class-activitypub-mention.php b/tests/test-class-activitypub-mention.php index 6f6b9ff..ca7395f 100644 --- a/tests/test-class-activitypub-mention.php +++ b/tests/test-class-activitypub-mention.php @@ -31,6 +31,7 @@ ENDPRE; array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), array( 'hallo @pfefferle@notiz.blog test', 'hallo @pfefferle@notiz.blog test' ), + array( 'hallo https://notiz.blog/@pfefferle/ test', 'hallo https://notiz.blog/@pfefferle/ test' ), array( $code, $code ), array( $pre, $pre ), ); From e16e119e6c5a909763e2b31b86331d23181911fe Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Apr 2023 10:45:35 +0200 Subject: [PATCH 075/427] Switch to general actions and filter As a result, we will not modify the images within the ActivityPub plugin, but the hooks will allow third-parties to do it on their end. See discussion: https://github.com/pfefferle/wordpress-activitypub/pull/309#issuecomment-1521488186 --- includes/model/class-post.php | 58 +++++++++++++++++------------------ readme.txt | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 254fb8c..c4632b6 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -305,7 +305,22 @@ class Post { // get URLs for each image foreach ( $image_ids as $id ) { - $thumbnail = $this->get_image( $id ); + $image_size = 'full'; + + /** + * Filter the image URL returned for each post. + * + * @param array|false $thumbnail The image URL, or false if no image is available. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. + */ + $thumbnail = apply_filters( + 'activitypub_get_image', + $this->get_image( $id, $image_size ), + $id, + $image_size + ); + if ( $thumbnail ) { $mimetype = \get_post_mime_type( $id ); $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); @@ -333,44 +348,29 @@ class Post { * Can return a CDNized URL if Jetpack's image CDN is active. * This can be disabled with a filter. * - * @param int $id The attachment ID. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. * * @return array|false Array of image data, or boolean false if no image is available. */ - public function get_image( $id ) { + public function get_image( $id, $image_size = 'full' ) { /** - * Allow bypassing Jetpack's Image CDN when returning image URLs. + * Hook into the image retrieval process. Before image retrieval. * - * @param bool $should_use_cdn Whether to use the Jetpack Image CDN. True by default. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. */ - $should_use_cdn = apply_filters( 'activitypub_images_use_jetpack_image_cdn', true ); + do_action( 'activitypub_get_image_pre', $id, $image_size ); - if ( $should_use_cdn ) { - // Return the full URL, using a CDN URL if Jetpack's image CDN is active. - return \wp_get_attachment_image_src( $id, 'full' ); - } + $thumbnail = \wp_get_attachment_image_src( $id, $image_size ); - /* - * Disable Jetpacks image CDN image processing for this request. + /** + * Hook into the image retrieval process. After image retrieval. * - * Note: old versions of Jetpack used the Jetpack_Photon class to do this. - * New versions use the Image_CDN class. - * Let's handle both. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'full' by default. */ - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \remove_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ) ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \remove_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ) ); - } - - $thumbnail = \wp_get_attachment_image_src( $id, 'full' ); - - // Re-enable Photon now that the image URL has been built. - if ( \class_exists( '\Automattic\Jetpack\Image_CDN\Image_CDN' ) ) { - \add_filter( 'image_downsize', array( \Automattic\Jetpack\Image_CDN\Image_CDN::instance(), 'filter_image_downsize' ), 10, 3 ); - } elseif ( \class_exists( 'Jetpack_Photon' ) ) { - \add_filter( 'image_downsize', array( \Jetpack_Photon::instance(), 'filter_image_downsize' ), 10, 3 ); - } + do_action( 'activitypub_get_image_pre', $id, $image_size ); return $thumbnail; } diff --git a/readme.txt b/readme.txt index 569408f..492942a 100644 --- a/readme.txt +++ b/readme.txt @@ -115,7 +115,7 @@ Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github = Next = -* Compatibility: add filter to allow disabling Jetpack's image CDN when returning images in ActivityPub requests. +* Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. = 0.17.0 = From bd75603fc77ce746ea7670fd7ee564b5d2f0c9b1 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Apr 2023 10:47:49 +0200 Subject: [PATCH 076/427] Remove comment about Jetpack's Photon --- includes/model/class-post.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index c4632b6..4294996 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -345,9 +345,6 @@ class Post { /** * Return details about an image attachment. * - * Can return a CDNized URL if Jetpack's image CDN is active. - * This can be disabled with a filter. - * * @param int $id The attachment ID. * @param string $image_size The image size to retrieve. Set to 'full' by default. * From 4a4a06de378f44dcdd9836506d0aca55ef62febb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:22:44 +0200 Subject: [PATCH 077/427] get_follower requires user_id check --- includes/collection/class-followers.php | 23 +++++++++++++++---- tests/test-class-db-activitypub-followers.php | 17 ++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 9669398..98971b9 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -183,12 +183,27 @@ class Followers { * * @param string $actor The Actor URL * - * @return \Activitypub\Model\Follower The Follower object + * @return \Activitypub\Model\Follower The Follower object */ - public static function get_follower( $actor ) { - $term = get_term_by( 'name', $actor, self::TAXONOMY ); + public static function get_follower( $user_id, $actor ) { + $terms = new WP_Term_Query( + array( + 'name' => $actor, + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => 1, + ) + ); - return new Follower( $term->name ); + $term = $terms->get_terms(); + + if ( is_array( $term ) && ! empty( $term ) ) { + $term = reset( $term ); + return new Follower( $term->name ); + } + + return null; } /** diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 0591178..97318d4 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -50,6 +50,23 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertContains( $follower, $db_followers ); } + public function test_get_follower() { + $followers = array( 'https://example.com/author/jon' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); + $this->assertNull( $follower ); + } + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { return true; From 0ee1266c30cb292fbab25485ac8f82aaa492d8c7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:23:28 +0200 Subject: [PATCH 078/427] add sanitize callbacks --- includes/collection/class-followers.php | 49 ++++++++++++++++++++++--- includes/model/class-follower.php | 8 ++-- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 98971b9..f520702 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -2,6 +2,7 @@ namespace Activitypub\Collection; use WP_Error; +use Exception; use WP_Term_Query; use Activitypub\Webfinger; use Activitypub\Model\Activity; @@ -67,7 +68,9 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_displayname' ), + 'sanitize_callback' => function( $value ) { + return sanitize_user( $value ); + }, ) ); @@ -77,7 +80,9 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_username' ), + 'sanitize_callback' => function( $value ) { + return sanitize_user( $value, true ); + }, ) ); @@ -87,7 +92,13 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_avatar' ), + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return ''; + } + + return esc_url_raw( $value ); + }, ) ); @@ -97,7 +108,29 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_inbox' ), + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + throw new Exception( '"inbox" has to be a valid URL' ); + } + + return esc_url_raw( $value ); + }, + ) + ); + + register_term_meta( + self::TAXONOMY, + 'shared_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return null; + } + + return esc_url_raw( $value ); + }, ) ); @@ -107,7 +140,13 @@ class Followers { array( 'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'validate_updated_at' ), + 'sanitize_callback' => function( $value ) { + if ( ! is_numeric( $value ) && (int) $value !== $value ) { + $value = strtotime( 'now' ); + } + + return $value; + }, ) ); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 84c6b0a..f3f8f7b 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -229,21 +229,21 @@ class Follower { foreach ( $this->map_meta as $remote => $internal ) { if ( ! empty( $meta[ $remote ] ) ) { - update_term_meta( $this->id, $internal, esc_html( $meta[ $remote ] ), true ); + update_term_meta( $this->id, $internal, $meta[ $remote ], true ); $this->$internal = $meta[ $remote ]; } } if ( ! empty( $meta['icon']['url'] ) ) { - update_term_meta( $this->id, 'avatar', esc_url_raw( $meta['icon']['url'] ), true ); + update_term_meta( $this->id, 'avatar', $meta['icon']['url'], true ); $this->avatar = $meta['icon']['url']; } if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['endpoints']['sharedInbox'] ), true ); + update_term_meta( $this->id, 'shared_inbox', $meta['endpoints']['sharedInbox'], true ); $this->shared_inbox = $meta['endpoints']['sharedInbox']; } elseif ( ! empty( $meta['inbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', esc_url_raw( $meta['inbox'] ), true ); + update_term_meta( $this->id, 'shared_inbox', $meta['inbox'], true ); $this->shared_inbox = $meta['inbox']; } From b8c86915b5dc79b39c8b412160067b57171ed99b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Apr 2023 17:24:27 +0200 Subject: [PATCH 079/427] add missing phpdoc --- includes/collection/class-followers.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index f520702..cd7f5f7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -208,8 +208,8 @@ class Followers { /** * Remove a Follower * - * @param int $user_id The ID of the WordPress User - * @param string $actor The Actor URL + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL * * @return bool|WP_Error True on success, false or WP_Error on failure. */ @@ -220,7 +220,8 @@ class Followers { /** * Remove a Follower * - * @param string $actor The Actor URL + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL * * @return \Activitypub\Model\Follower The Follower object */ From ec822535c9d49e1fc563facd42df0531c6f35d08 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 09:57:50 +0200 Subject: [PATCH 080/427] Follower object should not make any remote calls --- includes/collection/class-followers.php | 10 +- includes/functions.php | 8 +- includes/model/class-follower.php | 94 ++++++++++--------- includes/table/class-followers.php | 8 ++ tests/test-class-db-activitypub-followers.php | 39 +++++++- 5 files changed, 105 insertions(+), 54 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index cd7f5f7..3eb09d8 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -10,6 +10,7 @@ use Activitypub\Model\Follower; use function Activitypub\safe_remote_get; use function Activitypub\safe_remote_post; +use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection @@ -193,7 +194,14 @@ class Followers { * @return array|WP_Error The Follower (WP_Term array) or an WP_Error */ public static function add_follower( $user_id, $actor ) { + $meta = get_remote_metadata_by_actor( $actor ); + + if ( ! $meta || is_wp_error( $meta ) || ! is_array( $meta ) ) { + return $meta; + } + $follower = new Follower( $actor ); + $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -286,8 +294,6 @@ class Followers { * @return array The Term list of Followers, the format depends on $output */ public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { - //self::migrate_followers( $user_id ); - $terms = new WP_Term_Query( array( 'taxonomy' => self::TAXONOMY, diff --git a/includes/functions.php b/includes/functions.php index 09d3911..3a457a9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -102,12 +102,10 @@ function get_webfinger_resource( $user_id ) { * Requests the Meta-Data from the Actors profile * * @param string $actor The Actor URL - * @param bool $cache Enable/Disable caching of the meta. - * This does not effect Error-Caching. * * @return array The Actor profile as array */ -function get_remote_metadata_by_actor( $actor, $cache = false ) { +function get_remote_metadata_by_actor( $actor ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); if ( $pre ) { return $pre; @@ -161,9 +159,7 @@ function get_remote_metadata_by_actor( $actor, $cache = false ) { $metadata = \wp_remote_retrieve_body( $response ); $metadata = \json_decode( $metadata, true ); - if ( true === $cache ) { - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - } + \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); if ( ! $metadata ) { $metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor ); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index f3f8f7b..d4fc70f 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -3,8 +3,6 @@ namespace Activitypub\Model; use Activitypub\Collection\Followers; -use function Activitypub\get_remote_metadata_by_actor; - /** * ActivityPub Follower Class * @@ -111,16 +109,14 @@ class Follower { * @param WP_Post $post */ public function __construct( $actor ) { - $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); - $this->actor = $actor; + $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); + if ( $term ) { $this->id = $term->term_id; $this->slug = $term->slug; $this->meta = json_decode( $term->meta ); - } else { - $this->slug = sanitize_title( $actor ); } } @@ -147,46 +143,72 @@ class Follower { } } + public function from_meta( $meta ) { + $this->meta = $meta; + + foreach ( $this->map_meta as $remote => $internal ) { + if ( ! empty( $meta[ $remote ] ) ) { + $this->$internal = $meta[ $remote ]; + } + } + + if ( ! empty( $meta['icon']['url'] ) ) { + $this->avatar = $meta['icon']['url']; + } + + if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { + $this->shared_inbox = $meta['endpoints']['sharedInbox']; + } elseif ( ! empty( $meta['inbox'] ) ) { + $this->shared_inbox = $meta['inbox']; + } + + $this->updated_at = \strtotime( 'now' ); + } + public function get( $attribute ) { if ( $this->$attribute ) { return $this->$attribute; } - if ( ! $this->id ) { - $this->$attribute = $this->get_meta_by( $attribute ); - return $this->$attribute; + $attribute = get_term_meta( $this->id, $attribute, true ); + if ( $attribute ) { + $this->$attribute = $attribute; + return $attribute; } - $this->$attribute = get_term_meta( $this->id, $attribute, true ); - return $this->$attribute; + $attribute = $this->get_meta_by( $attribute ); + if ( $attribute ) { + $this->$attribute = $attribute; + return $attribute; + } + + return null; } - public function get_meta_by( $attribute, $force = false ) { - $meta = $this->get_meta( $force ); + public function get_meta_by( $attribute ) { + $meta = $this->get_meta(); + // try mapped data ($this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { return $meta[ $remote ]; } } + // try ActivityPub attribtes + if ( ! empty( $this->map_meta[ $attribute ] ) ) { + return $this->map_meta[ $attribute ]; + } + return null; } - public function get_meta( $force = false ) { - if ( $this->meta && false === (bool) $force ) { + public function get_meta() { + if ( $this->meta ) { return $this->meta; } - $remote_data = get_remote_metadata_by_actor( $this->actor ); - - if ( ! $remote_data || is_wp_error( $remote_data ) || ! is_array( $remote_data ) ) { - $remote_data = array(); - } - - $this->meta = $remote_data; - - return $this->meta; + return null; } public function update() { @@ -225,28 +247,12 @@ class Follower { } protected function update_term_meta() { - $meta = $this->get_meta(); + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); - foreach ( $this->map_meta as $remote => $internal ) { - if ( ! empty( $meta[ $remote ] ) ) { - update_term_meta( $this->id, $internal, $meta[ $remote ], true ); - $this->$internal = $meta[ $remote ]; + foreach ( $attributes as $attribute ) { + if ( $this->get( $attribute ) ) { + update_term_meta( $this->id, $attribute, $this->get( $attribute ), true ); } } - - if ( ! empty( $meta['icon']['url'] ) ) { - update_term_meta( $this->id, 'avatar', $meta['icon']['url'], true ); - $this->avatar = $meta['icon']['url']; - } - - if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', $meta['endpoints']['sharedInbox'], true ); - $this->shared_inbox = $meta['endpoints']['sharedInbox']; - } elseif ( ! empty( $meta['inbox'] ) ) { - update_term_meta( $this->id, 'shared_inbox', $meta['inbox'], true ); - $this->shared_inbox = $meta['inbox']; - } - - update_term_meta( $this->id, 'updated_at', \strtotime( 'now' ), true ); } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 13d6a91..c39b45f 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -77,6 +77,14 @@ class Followers extends WP_List_Table { ); } + public function column_identifier( $item ) { + return sprintf( + '%s', + $item['identifier'], + $item['identifier'] + ); + } + public function column_cb( $item ) { return sprintf( '', esc_attr( $item['identifier'] ) ); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 97318d4..640c4d1 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -13,14 +13,37 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { 'name' => 'jon', 'prefferedUsername' => 'jon', ), + 'doe@example.org' => array( + 'url' => 'https://example.org/author/doe', + 'inbox' => 'https://example.org/author/doe/inbox', + 'name' => 'doe', + 'prefferedUsername' => 'doe', + ), 'sally@example.org' => array( 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), + '12345@example.com' => array( + 'url' => 'https://12345.example.com', + 'inbox' => 'https://12345.example.com/inbox', + 'name' => '12345', + 'prefferedUsername' => '12345', + ), ); + public function set_up() { + parent::set_up(); + add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); + _delete_all_posts(); + } + + public function tear_down() { + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) ); + parent::tear_down(); + } + public function test_get_followers() { $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); @@ -28,7 +51,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); foreach ( $followers as $follower ) { - \Activitypub\Collection\Followers::add_follower( 1, $follower ); + $response = \Activitypub\Collection\Followers::add_follower( 1, $follower ); } $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); @@ -42,7 +65,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $follower = 'https://example.com/author/' . \time(); + $follower = 'https://12345.example.com'; \Activitypub\Collection\Followers::add_follower( 1, $follower ); $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); @@ -95,4 +118,16 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { public static function http_response( $response, $args, $url ) { return $response; } + + public static function pre_get_remote_metadata_by_actor( $pre, $actor ) { + if ( isset( self::$users[ $actor ] ) ) { + return self::$users[ $actor ]; + } + foreach ( self::$users as $username => $data ) { + if ( $data['url'] === $actor ) { + return $data; + } + } + return $pre; + } } From 230aaa5b244afc25058fd9284e50f5a96b452391 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:34:54 +0200 Subject: [PATCH 081/427] prepare migration --- activitypub.php | 2 + includes/class-http.php | 93 +++++++++++++++++++++++++ includes/class-migration.php | 68 ++++++++++++++++++ includes/collection/class-followers.php | 16 ----- includes/functions.php | 53 +------------- 5 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 includes/class-http.php create mode 100644 includes/class-migration.php diff --git a/activitypub.php b/activitypub.php index 6439ec8..41c01dd 100644 --- a/activitypub.php +++ b/activitypub.php @@ -32,8 +32,10 @@ function init() { \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php'; + require_once \dirname( __FILE__ ) . '/includes/class-http.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; + require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; diff --git a/includes/class-http.php b/includes/class-http.php new file mode 100644 index 0000000..7b570a2 --- /dev/null +++ b/includes/class-http.php @@ -0,0 +1,93 @@ + 100, + 'limit_response_size' => 1048576, + 'redirection' => 3, + 'user-agent' => "$user_agent; ActivityPub", + 'headers' => array( + 'Accept' => 'application/activity+json', + 'Content-Type' => 'application/activity+json', + 'Digest' => "SHA-256=$digest", + 'Signature' => $signature, + 'Date' => $date, + ), + 'body' => $body, + ); + + $response = \wp_safe_remote_get( $url, $args ); + $code = \wp_remote_retrieve_response_code( $response ); + + if ( 400 >= $code && 500 <= $code ) { + $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); + } + + \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); + + return $response; + } + + /** + * Send a GET Request with the needed HTTP Headers + * + * @param string $url The URL endpoint + * @param int $user_id The WordPress User-ID + * + * @return array|WP_Error The GET Response or an WP_ERROR + */ + public static function get( $url, $user_id ) { + $date = \gmdate( 'D, d M Y H:i:s T' ); + $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); + + $wp_version = \get_bloginfo( 'version' ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $args = array( + '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', + 'Content-Type' => 'application/activity+json', + 'Signature' => $signature, + 'Date' => $date, + ), + ); + + $response = \wp_safe_remote_get( $url, $args ); + $code = \wp_remote_retrieve_response_code( $response ); + + if ( 400 >= $code && 500 <= $code ) { + $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); + } + + \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); + + return $response; + } +} diff --git a/includes/class-migration.php b/includes/class-migration.php new file mode 100644 index 0000000..f7e3003 --- /dev/null +++ b/includes/class-migration.php @@ -0,0 +1,68 @@ + 'ID' ) ) as $user_id ) { + $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + + if ( $followes ) { + foreach ( $followes as $follower ) { + Collection\Followers::add_follower( $user_id, $follower ); + } + } + } + } +} diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 3eb09d8..597e0a7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -374,20 +374,4 @@ class Followers { return array_filter( $results ); } - - /** - * Migrate Followers - * - * @param int $user_id The ID of the WordPress User - * @return void - */ - public static function migrate_followers( $user_id ) { - $followes = get_user_meta( $user_id, 'activitypub_followers', true ); - - if ( $followes ) { - foreach ( $followes as $follower ) { - self::add_follower( $user_id, $follower ); - } - } - } } diff --git a/includes/functions.php b/includes/functions.php index 3a457a9..15068d0 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -33,58 +33,11 @@ function get_context() { } function safe_remote_post( $url, $body, $user_id ) { - $date = \gmdate( 'D, d M Y H:i:s T' ); - $digest = \Activitypub\Signature::generate_digest( $body ); - $signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); - - $wp_version = \get_bloginfo( 'version' ); - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - 'timeout' => 100, - 'limit_response_size' => 1048576, - 'redirection' => 3, - 'user-agent' => "$user_agent; ActivityPub", - 'headers' => array( - 'Accept' => 'application/activity+json', - 'Content-Type' => 'application/activity+json', - 'Digest' => "SHA-256=$digest", - 'Signature' => $signature, - 'Date' => $date, - ), - 'body' => $body, - ); - - $response = \wp_safe_remote_post( $url, $args ); - - \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); - - return $response; + return \Activitypub\Http::post( $url, $body, $user_id ); } function safe_remote_get( $url, $user_id ) { - $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); - - $wp_version = \get_bloginfo( 'version' ); - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - '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', - 'Content-Type' => 'application/activity+json', - 'Signature' => $signature, - 'Date' => $date, - ), - ); - - $response = \wp_safe_remote_get( $url, $args ); - - \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); - - return $response; + return \Activitypub\Http::get( $url, $user_id ); } /** @@ -149,7 +102,7 @@ function get_remote_metadata_by_actor( $actor ) { return 3; }; add_filter( 'activitypub_remote_get_timeout', $short_timeout ); - $response = \Activitypub\safe_remote_get( $actor, $user_id ); + $response = Http::get( $actor, $user_id ); remove_filter( 'activitypub_remote_get_timeout', $short_timeout ); if ( \is_wp_error( $response ) ) { \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. From 02e3488fd7b7036a8b1faefe7dd618c181cc4440 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:45:38 +0200 Subject: [PATCH 082/427] remove debugging stuff --- includes/class-migration.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index f7e3003..f3aac10 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -35,14 +35,14 @@ class Migration { */ public static function maybe_migrate() { if ( self::is_latest_version() ) { - //return; + return; } $version_from_db = self::get_version(); - //if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { + if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { self::migrate_to_1_0_0(); - //} + } update_option( 'activitypub_db_version', self::$target_version ); } From fb3d6d26349e97cd4e4a056c9b8934ff5b229d07 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 27 Apr 2023 14:49:39 +0200 Subject: [PATCH 083/427] fix phpcs --- includes/class-http.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 7b570a2..fd8df86 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -43,7 +43,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 >= $code && 500 <= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -52,7 +52,7 @@ class Http { return $response; } - /** + /** * Send a GET Request with the needed HTTP Headers * * @param string $url The URL endpoint @@ -82,7 +82,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 >= $code && 500 <= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } From 5ef41dea027e8199e4de9d7d371628e475255c33 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 09:54:09 +0200 Subject: [PATCH 084/427] schedule migration because it takes quite some time --- activitypub.php | 4 +++- includes/class-activity-dispatcher.php | 3 +++ includes/class-admin.php | 7 +++++++ includes/class-migration.php | 4 ++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 41c01dd..7523bcc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -35,7 +35,6 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/class-http.php'; require_once \dirname( __FILE__ ) . '/includes/class-signature.php'; require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php'; - require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php'; require_once \dirname( __FILE__ ) . '/includes/functions.php'; @@ -43,6 +42,9 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; + require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; + Migration::init(); + require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php'; Activity_Dispatcher::init(); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index e71ac19..422240d 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -61,6 +61,9 @@ class Activity_Dispatcher { * @return void */ public static function send_activity( Post $activitypub_post, $activity_type ) { + // check if a migration is needed before sending new posts + \Activitypub\Migration::maybe_migrate(); + // get latest version of post $user_id = $activitypub_post->get_post_author(); diff --git a/includes/class-admin.php b/includes/class-admin.php index c8c5813..c66c0f0 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,6 +13,7 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); + \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } @@ -144,6 +145,12 @@ class Admin { ); } + public static function schedule_migration() { + if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); + } + } + public static function add_settings_help_tab() { require_once \dirname( __FILE__ ) . '/help.php'; } diff --git a/includes/class-migration.php b/includes/class-migration.php index f3aac10..953ab6f 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -9,6 +9,10 @@ class Migration { */ private static $target_version = '1.0.0'; + public static function init() { + \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); + } + public static function get_target_version() { return self::$target_version; } From f2355cd9607c1d45708461cffcc230d8d2cdf3de Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 11:23:40 +0200 Subject: [PATCH 085/427] fix typo --- includes/class-http.php | 2 +- includes/collection/class-followers.php | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index fd8df86..01c2042 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -40,7 +40,7 @@ class Http { 'body' => $body, ); - $response = \wp_safe_remote_get( $url, $args ); + $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); if ( 400 >= $code && 500 <= $code ) { diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 597e0a7..0bd8d03 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -4,6 +4,7 @@ namespace Activitypub\Collection; use WP_Error; use Exception; use WP_Term_Query; +use Activitypub\Http; use Activitypub\Webfinger; use Activitypub\Model\Activity; use Activitypub\Model\Follower; @@ -269,6 +270,11 @@ class Followers { // @todo send error message //} + if ( isset( $object['user_id'] ) ) { + unset( $object['user_id'] ); + unset( $object['@context'] ); + } + // get inbox $inbox = $follower->get_inbox(); @@ -280,7 +286,7 @@ class Followers { $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); $activity = $activity->to_simple_json(); - $response = safe_remote_post( $inbox, $activity, $user_id ); + $response = Http::post( $inbox, $activity, $user_id ); } /** From 02e0acdf6917d05864b63748aac16dca37ce0aed Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 14:39:33 +0200 Subject: [PATCH 086/427] fix indents --- includes/class-admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index f1f7df8..2b7ee2a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -14,7 +14,7 @@ class Admin { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); \add_action( 'show_user_profile', array( self::class, 'add_fediverse_profile' ) ); - \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); + \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); } From 7c47f9a07c3640d06af0559a0a07ed159e88ab9c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 15:12:30 +0200 Subject: [PATCH 087/427] clean up admin settings --- includes/class-admin.php | 38 +++++++++++++++++-------------------- includes/functions.php | 19 ------------------- templates/user-settings.php | 29 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 templates/user-settings.php diff --git a/includes/class-admin.php b/includes/class-admin.php index 2b7ee2a..66966f9 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,6 +1,8 @@ ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); - ?> -

- ID ); - ?> -
- - - - - - - ID, ACTIVITYPUB_USER_DESCRIPTION_KEY, true ); + + \load_template( + ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php', + true, + array( + 'description' => $description, + ) + ); } public static function save_user_description( $user_id ) { - if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) ) { + if ( isset( $_REQUEST['_apnonce'] ) && ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) ) { return false; } if ( ! current_user_can( 'edit_user', $user_id ) ) { diff --git a/includes/functions.php b/includes/functions.php index a436496..4d44853 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -241,25 +241,6 @@ function get_follower_inboxes( $user_id, $cc = array() ) { return $inboxes; } -function get_identifier_settings( $user_id ) { - ?> - - - - - - - -
- - -

or

- -

-
- + + + + + + + + + + + + + +
+ + +

+ or + +

+ +

+
+ + + +

+
From 9cd33ad544f1f739a88f2dbaf4776daa1a48c962 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 18:13:16 +0200 Subject: [PATCH 088/427] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 953ab6f..c241c92 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -60,7 +60,7 @@ class Migration { */ public static function migrate_to_1_0_0() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { - $followes = get_user_meta( $user_id, 'activitypub_followers', true ); + $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followes ) { foreach ( $followes as $follower ) { From be73f99b5968dcd2d12105fb95fc7bc4267331ba Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 28 Apr 2023 18:13:59 +0200 Subject: [PATCH 089/427] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index c241c92..f6e6286 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -62,8 +62,8 @@ class Migration { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); - if ( $followes ) { - foreach ( $followes as $follower ) { + if ( $followers ) { + foreach ( $followers as $follower ) { Collection\Followers::add_follower( $user_id, $follower ); } } From 22946ec7798d39c5975b987db7fd9ecfe0cafe12 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:27:35 +0200 Subject: [PATCH 090/427] change migration script to match plugin version /cc @akirk --- activitypub.php | 35 +++++++++++++++++++++ includes/class-admin.php | 2 -- includes/class-migration.php | 57 ++++++++++++++++++++++++++--------- includes/model/class-post.php | 42 -------------------------- 4 files changed, 78 insertions(+), 58 deletions(-) diff --git a/activitypub.php b/activitypub.php index e754e93..6bf1783 100644 --- a/activitypub.php +++ b/activitypub.php @@ -150,3 +150,38 @@ function enable_buddypress_features() { Integration\Buddypress::init(); } add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); + +/** + * `get_plugin_data` wrapper + * + * @return array The plugin metadata array + */ +function get_plugin_meta( $default_headers = array() ) { + if ( ! $default_headers ) { + $default_headers = array( + 'Name' => 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + 'RequiresWP' => 'Requires at least', + 'RequiresPHP' => 'Requires PHP', + 'UpdateURI' => 'Update URI', + ); + } + + return \get_file_data( __FILE__, $default_headers, 'plugin' ); +} + +/** + * Plugin Version Number used for caching. + */ +function get_plugin_version() { + $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); + + return $meta['Version']; +} diff --git a/includes/class-admin.php b/includes/class-admin.php index 8e19c37..e7e2dc4 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -54,8 +54,6 @@ class Admin { switch ( $tab ) { case 'settings': - Post::upgrade_post_content_template(); - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; case 'welcome': diff --git a/includes/class-migration.php b/includes/class-migration.php index f6e6286..21e315f 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -2,19 +2,12 @@ namespace Activitypub; class Migration { - /** - * Which internal datastructure version we are running on. - * - * @var int - */ - private static $target_version = '1.0.0'; - public static function init() { \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); } public static function get_target_version() { - return self::$target_version; + return plugin_version(); } public static function get_version() { @@ -45,20 +38,19 @@ class Migration { $version_from_db = self::get_version(); if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::migrate_to_1_0_0(); + self::migrate_from_0_17(); + self::migrate_from_0_16(); } - update_option( 'activitypub_db_version', self::$target_version ); + update_option( 'activitypub_db_version', self::get_target_version() ); } /** - * The Migration for Plugin Version 1.0.0 and DB Version 1.0.0 - * - * @since 5.0.0 + * Updates the DB-schema of the followers-list * * @return void */ - public static function migrate_to_1_0_0() { + public static function migrate_from_0_17() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -69,4 +61,41 @@ class Migration { } } } + + /** + * Updates the custom template to use shortcodes instead of the deprecated templates. + * + * @return void + */ + public static function migrate_from_0_16() { + // Get the custom template. + $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); + + // If the old content exists but is a blank string, we're going to need a flag to updated it even + // after setting it to the default contents. + $need_update = false; + + // If the old contents is blank, use the defaults. + if ( '' === $old_content ) { + $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT; + $need_update = true; + } + + // Set the new content to be the old content. + $content = $old_content; + + // Convert old templates to shortcodes. + $content = \str_replace( '%title%', '[ap_title]', $content ); + $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content ); + $content = \str_replace( '%content%', '[ap_content]', $content ); + $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content ); + $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content ); + $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content ); + $content = \str_replace( '%tags%', '[ap_hashtags]', $content ); + + // Store the new template if required. + if ( $content !== $old_content || $need_update ) { + \update_option( 'activitypub_custom_post_content', $content ); + } + } } diff --git a/includes/model/class-post.php b/includes/model/class-post.php index a69e139..db56a45 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -511,48 +511,6 @@ class Post { return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]"; } - // Upgrade from old template codes to shortcodes. - $content = self::upgrade_post_content_template(); - - return $content; - } - - /** - * Updates the custom template to use shortcodes instead of the deprecated templates. - * - * @return string the updated template content - */ - public static function upgrade_post_content_template() { - // Get the custom template. - $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); - - // If the old content exists but is a blank string, we're going to need a flag to updated it even - // after setting it to the default contents. - $need_update = false; - - // If the old contents is blank, use the defaults. - if ( '' === $old_content ) { - $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT; - $need_update = true; - } - - // Set the new content to be the old content. - $content = $old_content; - - // Convert old templates to shortcodes. - $content = \str_replace( '%title%', '[ap_title]', $content ); - $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content ); - $content = \str_replace( '%content%', '[ap_content]', $content ); - $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content ); - $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content ); - $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content ); - $content = \str_replace( '%tags%', '[ap_hashtags]', $content ); - - // Store the new template if required. - if ( $content !== $old_content || $need_update ) { - \update_option( 'activitypub_custom_post_content', $content ); - } - return $content; } } From 725fc0cecd9ad946470c039e84b8ffd806b7ba18 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:29:29 +0200 Subject: [PATCH 091/427] fix function call --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 21e315f..b65ed90 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -7,7 +7,7 @@ class Migration { } public static function get_target_version() { - return plugin_version(); + return get_plugin_version(); } public static function get_version() { From 654cdd41740238d335ec5599526a24be88b14d22 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:37:11 +0200 Subject: [PATCH 092/427] Update includes/class-migration.php Co-authored-by: Alex Kirk --- includes/class-migration.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index b65ed90..74e6ee2 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -37,10 +37,12 @@ class Migration { $version_from_db = self::get_version(); - if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { - self::migrate_from_0_17(); + if ( version_compare( $version_from_db, '0.16.0', '<' ) ) { self::migrate_from_0_16(); } + if ( version_compare( $version_from_db, '0.17.0', '<' ) ) { + self::migrate_from_0_17(); + } update_option( 'activitypub_db_version', self::get_target_version() ); } From 857fae9db16392b1ff00959328bc555b7a648756 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 09:50:08 +0200 Subject: [PATCH 093/427] serve_request is not needed any more this was only for handling the signing, so no more need for that --- includes/rest/class-inbox.php | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4327587..29d0d16 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -19,7 +19,6 @@ class Inbox { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_unfollow' ), 10, 2 ); //\add_action( 'activitypub_inbox_like', array( self::class, 'handle_reaction' ), 10, 2 ); @@ -64,24 +63,6 @@ class Inbox { ); } - /** - * Hooks into the REST API request to verify the signature. - * - * @param bool $served Whether the request has already been served. - * @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response. - * @param WP_REST_Request $request Request used to generate the response. - * @param WP_REST_Server $server Server instance. - * - * @return true - */ - public static function serve_request( $served, $result, $request, $server ) { - if ( '/activitypub' !== \substr( $request->get_route(), 0, 12 ) ) { - return $served; - } - - return $served; - } - /** * Renders the user-inbox * From 66942e6c622ec6dc4778ca0f1697f87491df9dcb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 13:54:21 +0200 Subject: [PATCH 094/427] fix error detection --- includes/class-http.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 01c2042..798328b 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -43,7 +43,7 @@ class Http { $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 <= $code && 500 >= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } @@ -82,7 +82,7 @@ class Http { $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( 400 >= $code && 500 <= $code ) { + if ( 400 <= $code && 500 >= $code ) { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } From 077c43bf95917252648e7c83ec8e068defe0ffa0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 14:35:53 +0200 Subject: [PATCH 095/427] single migration scripts should not be public --- includes/class-migration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 74e6ee2..619fc87 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -52,7 +52,7 @@ class Migration { * * @return void */ - public static function migrate_from_0_17() { + private static function migrate_from_0_17() { foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -69,7 +69,7 @@ class Migration { * * @return void */ - public static function migrate_from_0_16() { + private static function migrate_from_0_16() { // Get the custom template. $old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); From dea5f385613b67fa03498699785806c62c854320 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 2 May 2023 14:39:25 +0200 Subject: [PATCH 096/427] better error handling --- includes/collection/class-followers.php | 69 +++++++++++++++++++------ includes/functions.php | 21 ++++++++ includes/model/class-follower.php | 62 +++++++++++++++++++++- includes/table/class-followers.php | 22 ++++---- 4 files changed, 146 insertions(+), 28 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 0bd8d03..e21ce2d 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -9,8 +9,7 @@ use Activitypub\Webfinger; use Activitypub\Model\Activity; use Activitypub\Model\Follower; -use function Activitypub\safe_remote_get; -use function Activitypub\safe_remote_post; +use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; /** @@ -152,6 +151,22 @@ class Followers { ) ); + register_term_meta( + self::TAXONOMY, + 'errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function( $value ) { + if ( ! is_string( $value ) ) { + throw new Exception( 'Error message is no valid string' ); + } + + return esc_sql( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_taxonomy' ); } @@ -197,12 +212,16 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( ! $meta || is_wp_error( $meta ) || ! is_array( $meta ) ) { - return $meta; + $follower = new Follower( $actor ); + + if ( is_tombstone( $meta ) ) { + return; + } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_error( $meta ); + } else { + $follower->from_meta( $meta ); } - $follower = new Follower( $actor ); - $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -299,19 +318,29 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null ) { - $terms = new WP_Term_Query( - array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, - 'number' => $number, - 'offset' => $offset, - 'orderby' => 'id', - 'order' => 'ASC', - ) + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $hide_errors = false, $args = array() ) { + $defaults = array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + 'object_ids' => $user_id, + 'number' => $number, + 'offset' => $offset, + 'orderby' => 'id', + 'order' => 'ASC', ); + if ( true === $hide_errors ) { + $defaults['meta_query'] = array( + array( + 'key' => 'errors', + 'compare' => 'NOT EXISTS', + ), + ); + } + + $args = wp_parse_args( $args, $defaults ); + $terms = new WP_Term_Query( $args ); + $items = array(); // change output format @@ -358,6 +387,12 @@ class Followers { 'hide_empty' => false, 'object_ids' => $user_id, 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'inbox', + 'compare' => 'EXISTS', + ), + ), ) ); diff --git a/includes/functions.php b/includes/functions.php index b88f8a8..5fa7ab3 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -211,3 +211,24 @@ function get_author_description( $user_id ) { } return $description; } + +/** + * Check for Tombstone Objects + * + * @see https://www.w3.org/TR/activitypub/#delete-activity-outbox + * + * @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request + * + * @return boolean true if HTTP-Code is 410 or 404 + */ +function is_tombstone( $wp_error ) { + if ( ! is_wp_error( $wp_error ) ) { + return false; + } + + if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) { + return true; + } + + return false; +} diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index d4fc70f..694489d 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -92,6 +92,22 @@ class Follower { */ private $meta; + /** + * The latest received error. + * + * This will only temporary and will saved to $this->errors + * + * @var string + */ + private $error; + + /** + * A list of errors + * + * @var array + */ + private $errors; + /** * Maps the meta fields to the local db fields * @@ -185,10 +201,39 @@ class Follower { return null; } + public function get_errors() { + if ( $this->errors ) { + return $this->errors; + } + + $this->errors = get_term_meta( $this->id, 'errors' ); + return $this->errors; + } + + public function count_errors() { + $errors = $this->get_errors(); + + if ( is_array( $errors ) && ! empty( $errors ) ) { + return count( $errors ); + } + + return 0; + } + + public function get_latest_error_message() { + $errors = $this->get_errors(); + + if ( is_array( $errors ) && ! empty( $errors ) ) { + return reset( $errors ); + } + + return ''; + } + public function get_meta_by( $attribute ) { $meta = $this->get_meta(); - // try mapped data ($this->map_meta) + // try mapped data (see $this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { return $meta[ $remote ]; @@ -251,8 +296,21 @@ class Follower { foreach ( $attributes as $attribute ) { if ( $this->get( $attribute ) ) { - update_term_meta( $this->id, $attribute, $this->get( $attribute ), true ); + update_term_meta( $this->id, $attribute, $this->get( $attribute ) ); } } + + if ( $this->error ) { + if ( is_string( $this->error ) ) { + $error = $this->error; + } elseif ( is_wp_error( $this->error ) ) { + $error = $this->error->get_error_message(); + } else { + $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + } + + add_term_meta( $this->id, 'errors', $error ); + } + } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index c39b45f..c9b5948 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -11,11 +11,13 @@ if ( ! \class_exists( '\WP_List_Table' ) ) { class Followers extends WP_List_Table { public function get_columns() { return array( - 'cb' => '', - 'avatar' => \__( 'Avatar', 'activitypub' ), - 'name' => \__( 'Name', 'activitypub' ), - 'username' => \__( 'Username', 'activitypub' ), - 'identifier' => \__( 'Identifier', 'activitypub' ), + 'cb' => '', + 'avatar' => \__( 'Avatar', 'activitypub' ), + 'name' => \__( 'Name', 'activitypub' ), + 'username' => \__( 'Username', 'activitypub' ), + 'identifier' => \__( 'Identifier', 'activitypub' ), + 'errors' => \__( 'Errors', 'activitypub' ), + 'latest-error' => \__( 'Latest Error Message', 'activitypub' ), ); } @@ -47,10 +49,12 @@ class Followers extends WP_List_Table { foreach ( $follower as $follower ) { $item = array( - 'avatar' => esc_attr( $follower->get_avatar() ), - 'name' => esc_attr( $follower->get_name() ), - 'username' => esc_attr( $follower->get_username() ), - 'identifier' => esc_attr( $follower->get_actor() ), + 'avatar' => esc_attr( $follower->get_avatar() ), + 'name' => esc_attr( $follower->get_name() ), + 'username' => esc_attr( $follower->get_username() ), + 'identifier' => esc_attr( $follower->get_actor() ), + 'errors' => $follower->count_errors(), + 'latest-error' => $follower->get_latest_error_message(), ); $this->items[] = $item; From be0f25f3d388d0878339f75a980d554cf67b851b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:50:16 +0200 Subject: [PATCH 097/427] fail if `get_remote_metadata_by_actor` returns error because it is not even possible to send `Accept` or `Reject` response. --- includes/collection/class-followers.php | 30 ++++++++----------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index e21ce2d..7412d52 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -212,16 +212,11 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - $follower = new Follower( $actor ); - - if ( is_tombstone( $meta ) ) { - return; - } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - $follower->set_error( $meta ); - } else { - $follower->from_meta( $meta ); + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + return $meta; } + $follower = new Follower( $actor ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); @@ -285,9 +280,11 @@ class Followers { * @return void */ public static function send_follow_response( $actor, $object, $user_id, $follower ) { - //if ( is_wp_error( $follower ) ) { - // @todo send error message - //} + if ( is_wp_error( $follower ) ) { + // it is not even possible to send a "Reject" because + // we can not get the Remote-Inbox + return; + } if ( isset( $object['user_id'] ) ) { unset( $object['user_id'] ); @@ -318,7 +315,7 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $hide_errors = false, $args = array() ) { + public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) { $defaults = array( 'taxonomy' => self::TAXONOMY, 'hide_empty' => false, @@ -329,15 +326,6 @@ class Followers { 'order' => 'ASC', ); - if ( true === $hide_errors ) { - $defaults['meta_query'] = array( - array( - 'key' => 'errors', - 'compare' => 'NOT EXISTS', - ), - ); - } - $args = wp_parse_args( $args, $defaults ); $terms = new WP_Term_Query( $args ); From 72f72e79b85a4386bb90337677bf5cdf7342838f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:50:36 +0200 Subject: [PATCH 098/427] use custom (more error tolerant) version for migration --- includes/class-migration.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 619fc87..1e8c44b 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,6 +1,8 @@ set_error( $meta ); + } else { + $follower->from_meta( $meta ); + } + + $follower->upsert(); + + $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); } } } From 7127b0a5688c317de0a89a00dfc4ea46812e4b28 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 14:54:34 +0200 Subject: [PATCH 099/427] oops --- includes/collection/class-followers.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 7412d52..c6c6223 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -217,6 +217,7 @@ class Followers { } $follower = new Follower( $actor ); + $follower->from_meta( $meta ); $follower->upsert(); $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); From f07869c7d1b864a690dde6cd3d9d480858f04ca8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 3 May 2023 15:11:20 +0200 Subject: [PATCH 100/427] be sure to always update date --- includes/model/class-follower.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 694489d..3a15690 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -265,6 +265,7 @@ class Follower { ) ); + $this->updated_at = \strtotime( 'now' ); $this->update_term_meta(); } From 144356bf8aef5c04eeda17ffca2f7c67a5b7f8dc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 08:50:44 +0200 Subject: [PATCH 101/427] remove unused second param --- includes/class-mention.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-mention.php b/includes/class-mention.php index 96bb450..49f7860 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -69,7 +69,7 @@ class Mention { * @return string the final string */ public static function replace_with_links( $result ) { - $metadata = get_remote_metadata_by_actor( $result[0], true ); + $metadata = get_remote_metadata_by_actor( $result[0] ); if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) { $username = ltrim( $result[0], '@' ); if ( ! empty( $metadata['name'] ) ) { From 0e193914fa50e127460264f2f5281375c896a65d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 09:01:23 +0200 Subject: [PATCH 102/427] update URLs --- README.md | 8 ++++++-- readme.txt | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1b4461..d7a9662 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Contributors:** [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [akirk](https://profiles.wordpress.org/akirk/), [automattic](https://profiles.wordpress.org/automattic/) **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 -**Tested up to:** 6.1 +**Tested up to:** 6.2 **Stable tag:** 0.17.0 **Requires PHP:** 5.6 **License:** MIT @@ -111,7 +111,11 @@ Where 'blog' is the path to the subdirectory at which your blog resides. ## Changelog ## -Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). + +### Next ### + +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. ### 0.17.0 ### diff --git a/readme.txt b/readme.txt index e89c768..3f9f7f0 100644 --- a/readme.txt +++ b/readme.txt @@ -111,7 +111,7 @@ Where 'blog' is the path to the subdirectory at which your blog resides. == Changelog == -Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). +Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). = Next = From e489a048808abd285fdedc048971d6fc673f8985 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 4 May 2023 09:32:52 +0200 Subject: [PATCH 103/427] remove unused constants --- activitypub.php | 3 +-- includes/class-admin.php | 4 ++-- includes/functions.php | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index 38886ff..d61af85 100644 --- a/activitypub.php +++ b/activitypub.php @@ -23,9 +23,8 @@ function init() { \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_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '

+ From 6ba8156e50e2904fa61a1b28cf9df3632bed8e2f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 5 May 2023 14:40:17 +0200 Subject: [PATCH 110/427] fix #320 --- includes/model/class-post.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 2b6c84c..ccb161c 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -109,6 +109,12 @@ class Post { ), 'blockquote' => array(), 'cite' => array(), + 'code' => array( + 'class' => array(), + ), + 'pre' => array( + 'class' => array(), + ), ); /** From 6b68f0763d6cd6966ba36c55d97697a13d912bfe Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 26 Apr 2023 15:54:09 -0600 Subject: [PATCH 111/427] hold off secure mode --- includes/class-admin.php | 18 +++++++++--------- includes/rest/class-server.php | 14 +++++++------- templates/settings.php | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 7a19e8e..4bc7b9c 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - \register_setting( - 'activitypub', - 'activitypub_use_secure_mode', - array( - 'type' => 'boolean', - 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - 'default' => 0, - ) - ); + // \register_setting( + // 'activitypub', + // 'activitypub_use_secure_mode', + // array( + // 'type' => 'boolean', + // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + // 'default' => 0, + // ) + // ); } public static function schedule_migration() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 790728a..bf1cd5e 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -42,13 +42,13 @@ class Server { } } else { // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - - if ( $secure_mode ) { - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } - } + // $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + // if ( $secure_mode ) { + // $verified_request = Signature::verify_http_signature( $request ); + // if ( \is_wp_error( $verified_request ) ) { + // return $verified_request; + // } + // } } } } diff --git a/templates/settings.php b/templates/settings.php index 6429688..28d7f32 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@ - + From 2bebc88b785fedbd644424b3415e120f97fae36e Mon Sep 17 00:00:00 2001 From: Django Date: Fri, 5 May 2023 11:47:52 -0600 Subject: [PATCH 112/427] fix undefined get_remote_metadata_by_actor Not tested --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 1e8c44b..53d3770 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -59,7 +59,7 @@ class Migration { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followers ) { - foreach ( $followers as $follower ) { + foreach ( $followers as $actor ) { $meta = get_remote_metadata_by_actor( $actor ); $follower = new Follower( $actor ); From 27636b62d50c81d05105e343030daa01a0b18aa9 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:02:12 -0600 Subject: [PATCH 113/427] Add Service actor for signing get requests --- includes/class-http.php | 4 +-- includes/class-signature.php | 39 +++++++++++++++++--------- includes/functions.php | 16 ++--------- includes/rest/class-server.php | 50 +++++++++++++++++++++++++++++++++- 4 files changed, 80 insertions(+), 29 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 798328b..e6a9a79 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -60,9 +60,9 @@ class Http { * * @return array|WP_Error The GET Response or an WP_ERROR */ - public static function get( $url, $user_id ) { + public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( $user_id, 'get', $url, $date ); + $signature = Signature::generate_signature( -1, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index 608bff3..dcf3d96 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -35,14 +35,15 @@ class Signature { * @return mixed */ public static function get_private_key( $user_id, $force = false ) { - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); - - if ( $key && ! $force ) { - return $key[0]; + if ( $force ) { + self::generate_key_pair( $user_id ); } - self::generate_key_pair( $user_id ); - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + if ( -1 === $user_id ) { + $key = \get_option('activitypub_magic_sig_private_key' ); + } else { + $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + } return $key[0]; } @@ -63,14 +64,22 @@ class Signature { $priv_key = null; \openssl_pkey_export( $key, $priv_key ); - - // private key - \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); - $detail = \openssl_pkey_get_details( $key ); - // public key - \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); + if ( -1 === $user_id ) { + // private key + \add_option('activitypub_magic_sig_private_key', $priv_key ); + + // public key + \add_option('activitypub_magic_sig_public_key', $detail['key'] ); + + } else { + // private key + \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); + + // public key + \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); + } } public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { @@ -101,7 +110,11 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - $key_id = \get_author_posts_url( $user_id ) . '#main-key'; + if ( -1 === $user_id ) { + $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); + } else { + $key_id = \get_author_posts_url( $user_id ) . '#main-key'; + } if ( ! empty( $digest ) ) { return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature ); diff --git a/includes/functions.php b/includes/functions.php index bd04e6c..9743b44 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -36,8 +36,8 @@ function safe_remote_post( $url, $body, $user_id ) { return \Activitypub\Http::post( $url, $body, $user_id ); } -function safe_remote_get( $url, $user_id ) { - return \Activitypub\Http::get( $url, $user_id ); +function safe_remote_get( $url ) { + return \Activitypub\Http::get( $url ); } /** @@ -88,21 +88,11 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } - $user = \get_users( - array( - 'number' => 1, - 'capability__in' => array( 'publish_posts' ), - 'fields' => 'ID', - ) - ); - - // we just need any user to generate a request signature - $user_id = \reset( $user ); $short_timeout = function() { return 3; }; add_filter( 'activitypub_remote_get_timeout', $short_timeout ); - $response = Http::get( $actor, $user_id ); + $response = Http::get( $actor ); remove_filter( 'activitypub_remote_get_timeout', $short_timeout ); if ( \is_wp_error( $response ) ) { \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index bf1cd5e..5934bcf 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -1,6 +1,7 @@ \WP_REST_Server::READABLE, + 'callback' => array( self::class, 'service_actor' ), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Render Service actor profile + * + * @return WP_REST_Response + */ + public static function service_actor() { + $json = new \stdClass(); + + $json->{'@context'} = \Activitypub\get_context(); + $json->id = \get_rest_url( null, 'activitypub/1.0/service' ); + $json->type = 'Application'; + $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); + $json->name = get_bloginfo( 'name' ); + $json->summary = "ActivityPub service actor"; + $json->manuallyApprovesFollowers = TRUE; + $json->icon = [ get_site_icon_url() ]; + $json->publicKey = (object) array( + 'id' => \get_rest_url( null, 'activitypub/1.0/service#main-key' ), + 'owner' => \get_rest_url( null, 'activitypub/1.0/service' ), + 'publicKeyPem' => Signature::get_public_key( -1 ), + ); + + $response = new WP_REST_Response( $json, 200 ); + + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; } /** From e827221ee6b13ea4839fe920843b1fce12ea0e9b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:09:12 -0600 Subject: [PATCH 114/427] service actor as application actor --- includes/rest/class-server.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 5934bcf..6ee28d5 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -27,11 +27,11 @@ class Server { public static function register_routes() { \register_rest_route( 'activitypub/1.0', - '/service', + '/application', array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'service_actor' ), + 'callback' => array( self::class, 'application_actor' ), 'permission_callback' => '__return_true', ), ) @@ -39,24 +39,24 @@ class Server { } /** - * Render Service actor profile + * Render Application actor profile * * @return WP_REST_Response */ - public static function service_actor() { + public static function application_actor() { $json = new \stdClass(); $json->{'@context'} = \Activitypub\get_context(); - $json->id = \get_rest_url( null, 'activitypub/1.0/service' ); + $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); $json->name = get_bloginfo( 'name' ); - $json->summary = "ActivityPub service actor"; + $json->summary = "WordPress-ActivityPub application actor"; $json->manuallyApprovesFollowers = TRUE; $json->icon = [ get_site_icon_url() ]; $json->publicKey = (object) array( - 'id' => \get_rest_url( null, 'activitypub/1.0/service#main-key' ), - 'owner' => \get_rest_url( null, 'activitypub/1.0/service' ), + 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), + 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), 'publicKeyPem' => Signature::get_public_key( -1 ), ); From 35496f5026d06ebbc34cd7ad971eb6b62101920d Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:52:24 -0600 Subject: [PATCH 115/427] get_public_key support application actor --- includes/class-signature.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index dcf3d96..f735fc3 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -17,14 +17,15 @@ class Signature { * @return mixed */ public static function get_public_key( $user_id, $force = false ) { - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); - - if ( $key && ! $force ) { - return $key[0]; + if ( $force ) { + self::generate_key_pair( $user_id ); } - self::generate_key_pair( $user_id ); - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + if ( -1 === $user_id ) { + $key = array( \get_option('activitypub_magic_sig_public_key' ) ); + } else { + $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + } return $key[0]; } From c5ca061805257d558ac2e2cf00be4586b61e4de7 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:53:43 -0600 Subject: [PATCH 116/427] Add helper format_server_request --- includes/class-signature.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index f735fc3..ae4ff61 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -319,4 +319,29 @@ class Signature { $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore return "$digest"; } + + /** + * Formats the $_SERVER to resemble the WP_REST_REQUEST array, + * for use with verify_http_signature() + * + * @param array $_SERVER + * @return array $request + */ + public static function format_server_request( $server ) { + $request = array(); + foreach ( $server as $param_key => $param_val ) { + $req_param = strtolower( $param_key ); + if ( 'REQUEST_URI' === $req_param ) { + $request['headers']['route'][] = $param_val; + } else { + $header_key = str_replace( + 'http_', + '', + $req_param + ); + $request['headers'][ $header_key ][] = \wp_unslash( $param_val ); + } + } + return $request; + } } From 9d30f2c1dd899a1bade19f030381b39ff804561c Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 12:55:12 -0600 Subject: [PATCH 117/427] phpcbf --- includes/rest/class-server.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 6ee28d5..03e72cc 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -49,15 +49,15 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; - $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); + $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore snake_case $json->name = get_bloginfo( 'name' ); - $json->summary = "WordPress-ActivityPub application actor"; - $json->manuallyApprovesFollowers = TRUE; - $json->icon = [ get_site_icon_url() ]; - $json->publicKey = (object) array( + $json->summary = 'WordPress-ActivityPub application actor'; + $json->manuallyApprovesFollowers = true; // phpcs:ignore snake_case + $json->icon = [ get_site_icon_url() ]; // phpcs:ignore short array syntax + $json->publicKey = (object) array( // phpcs:ignore snake_case 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), + 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore snake_case ); $response = new WP_REST_Response( $json, 200 ); From 14f3c3985b843bba51e6bbd29a18a5468cb2af97 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:00:21 -0600 Subject: [PATCH 118/427] code style --- includes/class-signature.php | 45 +++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index ae4ff61..59d20c7 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -22,7 +22,7 @@ class Signature { } if ( -1 === $user_id ) { - $key = array( \get_option('activitypub_magic_sig_public_key' ) ); + $key = array( \get_option( 'activitypub_magic_sig_public_key' ) ); } else { $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); } @@ -41,7 +41,7 @@ class Signature { } if ( -1 === $user_id ) { - $key = \get_option('activitypub_magic_sig_private_key' ); + $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); } @@ -69,10 +69,10 @@ class Signature { if ( -1 === $user_id ) { // private key - \add_option('activitypub_magic_sig_private_key', $priv_key ); + \add_option( 'activitypub_magic_sig_private_key', $priv_key ); // public key - \add_option('activitypub_magic_sig_public_key', $detail['key'] ); + \add_option( 'activitypub_magic_sig_public_key', $detail['key'] ); } else { // private key @@ -127,19 +127,31 @@ class Signature { /** * Verifies the http signatures * - * @param WP_REQUEST | Array $request + * @param WP_REQUEST | Array $_SERVER * @return void * @author Django Doucet */ public static function verify_http_signature( $request ) { - $headers = $request->get_headers(); - - if ( ! $headers ) { - return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); + if ( is_object( $request ) ) { // REST Request object + $headers = $request->get_headers(); + error_log( 'verify: $request: ' . print_r( $request, true ) ); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + error_log( 'request $headers: ' . print_r( $headers['(request-target)'], true ) ); + } else { + $request = self::format_server_request( $request ); + $headers = $request['headers']; // $_SERVER array + // error_log( print_r( $headers, true ) ); + $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; + // $post = get_page_by_path( $headers['request_uri'], ARRAY_A ); + // $actor = post['post_author'] ?? ''; + $actor = ''; + error_log( 'request $headers: ' . print_r( $headers, true ) ); } - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + if ( ! isset( $headers['signature'] ) ) { + return new \WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); + } if ( array_key_exists( 'signature', $headers ) ) { $signature_block = self::parse_signature_header( $headers['signature'] ); @@ -183,7 +195,8 @@ class Signature { } } - $public_key = \Activitypub\get_publickey_by_actor( $actor, $signature_block['keyId'] ); // phpcs:ignore + strtok( $signature_block['keyId'], '?'); + $public_key = \Activitypub\get_remote_metadata_by_actor( $signature_block['keyId'] ); // phpcs:ignore if ( \is_wp_error( $public_key ) ) { return $public_key; } else { @@ -191,9 +204,9 @@ class Signature { } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { - return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); // phpcs:ignore null coalescing operator + return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } - + return $verified; } /** @@ -220,7 +233,7 @@ class Signature { * * @param array $header * @return array signature parts - * @author Django Doucet + * @author Django Doucet */ public static function parse_signature_header( $header ) { $ret = array(); @@ -258,7 +271,7 @@ class Signature { * * @param array $signed_headers * @param array $signature_block (pseudo-headers) - * @param array $headers (original http headers) + * @param array $headers (http headers) * @return signed headers for comparison * @author Django Doucet */ From 656a2b0f44009ef5d998c8b2ee89bfe5fc33e6da Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:22:47 -0600 Subject: [PATCH 119/427] remove unneeded filter --- includes/rest/class-inbox.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 217e816..64e4a0c 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -18,7 +18,6 @@ class Inbox { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_filter( 'rest_pre_serve_request', array( self::class, 'serve_request' ), 11, 4 ); \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 ); } From 0b4bada2b6d46bdf5a901a3bab6986a70c5f2e91 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:24:59 -0600 Subject: [PATCH 120/427] enable secure mode --- includes/class-admin.php | 18 +++++++++--------- templates/settings.php | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 4bc7b9c..7a19e8e 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - // \register_setting( - // 'activitypub', - // 'activitypub_use_secure_mode', - // array( - // 'type' => 'boolean', - // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - // 'default' => 0, - // ) - // ); + \register_setting( + 'activitypub', + 'activitypub_use_secure_mode', + array( + 'type' => 'boolean', + 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + 'default' => 0, + ) + ); } public static function schedule_migration() { diff --git a/templates/settings.php b/templates/settings.php index 28d7f32..6429688 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@
- + From 6c95a23d105b1dbe70e475b6641391796fc631cf Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 13:45:38 -0600 Subject: [PATCH 121/427] phpcbf --- includes/rest/class-server.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 03e72cc..fb3e569 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -49,15 +49,15 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); $json->type = 'Application'; - $json->preferredUsername = parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore snake_case + $json->preferredUsername = wp_parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; - $json->manuallyApprovesFollowers = true; // phpcs:ignore snake_case - $json->icon = [ get_site_icon_url() ]; // phpcs:ignore short array syntax - $json->publicKey = (object) array( // phpcs:ignore snake_case + $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions + $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax + $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore snake_case + 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore WordPress.NamingConventions ); $response = new WP_REST_Response( $json, 200 ); From 9202c1973070f97c3fc028c75cddb6025ff68fe4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 14:39:33 -0600 Subject: [PATCH 122/427] Add secure mode to REST get requests --- includes/class-signature.php | 3 +-- includes/rest/class-server.php | 14 +++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 59d20c7..47ab002 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -195,8 +195,7 @@ class Signature { } } - strtok( $signature_block['keyId'], '?'); - $public_key = \Activitypub\get_remote_metadata_by_actor( $signature_block['keyId'] ); // phpcs:ignore + $public_key = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $signature_block['keyId'] ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $public_key ) ) { return $public_key; } else { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index fb3e569..585b68b 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -90,13 +90,13 @@ class Server { } } else { // SecureMode/Authorized fetch. - // $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - // if ( $secure_mode ) { - // $verified_request = Signature::verify_http_signature( $request ); - // if ( \is_wp_error( $verified_request ) ) { - // return $verified_request; - // } - // } + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } } } } From 3d4ae84573a31f7ee863499129d6272b525085b2 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 14:40:30 -0600 Subject: [PATCH 123/427] Add secure mode to content negotiated requests --- includes/class-activitypub.php | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 16a1f22..c0695a9 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -1,6 +1,8 @@ Date: Fri, 5 May 2023 14:43:05 -0600 Subject: [PATCH 124/427] removing logging --- includes/class-signature.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 47ab002..7310e3a 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -134,19 +134,13 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); - error_log( 'verify: $request: ' . print_r( $request, true ) ); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); - error_log( 'request $headers: ' . print_r( $headers['(request-target)'], true ) ); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array - // error_log( print_r( $headers, true ) ); $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; - // $post = get_page_by_path( $headers['request_uri'], ARRAY_A ); - // $actor = post['post_author'] ?? ''; $actor = ''; - error_log( 'request $headers: ' . print_r( $headers, true ) ); } if ( ! isset( $headers['signature'] ) ) { From f79200ef27ac85875363ad28afac5bdc1e628227 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:44:15 -0600 Subject: [PATCH 125/427] make webfinger route available unsigned --- includes/rest/class-server.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 585b68b..0e8df43 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -79,22 +79,22 @@ class Server { * @return mixed|\WP_Error */ public static function authorize_activitypub_requests( $response, $handler, $request ) { - - $maybe_activitypub = $request->get_route(); - if ( str_starts_with( $maybe_activitypub, '/activitypub' ) ) { + $route = $request->get_route(); + if ( str_starts_with( $route, '/activitypub' ) ) { if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { return $verified_request; } } else { - // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; + if ( '/activitypub/1.0/webfinger' !== $route ) { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } } } } From 0d5c249eafc4f74d6c8eed59f86198316f4099a4 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:44:55 -0600 Subject: [PATCH 126/427] remove user_id variable from activitypub_safe_remote_get_response --- includes/class-http.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-http.php b/includes/class-http.php index e6a9a79..247d87e 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -86,7 +86,7 @@ class Http { $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ) ); } - \do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id ); + \do_action( 'activitypub_safe_remote_get_response', $response, $url ); return $response; } From dc8e1e0f3e5fe4edf4bd65d89bd165f47a25888b Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:50:49 -0600 Subject: [PATCH 127/427] fix request-target route, remove $actor from verify_http_signature --- includes/class-signature.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 7310e3a..2a4ea53 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -134,13 +134,11 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); - $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /wp-json' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; - $actor = ''; } if ( ! isset( $headers['signature'] ) ) { From afafdf1543f451a28b7ecc993bcc978edb199a89 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:54:29 -0600 Subject: [PATCH 128/427] Add get_remote_key method --- includes/class-signature.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 2a4ea53..05d2165 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -187,7 +187,7 @@ class Signature { } } - $public_key = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $signature_block['keyId'] ), '?' ) ); // phpcs:ignore + $public_key = self::get_remote_key( $signature_block['keyId'] ); if ( \is_wp_error( $public_key ) ) { return $public_key; } else { @@ -200,6 +200,24 @@ class Signature { return $verified; } + /** + * Get public key from key_id + * + * @param string $key_id + * @return string $publicKeyPem + * @author Django Doucet + */ + public static function get_remote_key( $key_id ) { // phpcs:ignore + $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore + if ( \is_wp_error( $actor ) ) { + return $actor; + } + if ( isset( $actor['publicKey']['publicKeyPem'] ) ) { + return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore + } + return null; + } + /** * Gets the signature algorithm from the signature header * From abedf014ae470062362878ee3e7b550a86f543ee Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 5 May 2023 23:56:39 -0600 Subject: [PATCH 129/427] remove redundant --- includes/class-signature.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 05d2165..c426727 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -190,8 +190,6 @@ class Signature { $public_key = self::get_remote_key( $signature_block['keyId'] ); if ( \is_wp_error( $public_key ) ) { return $public_key; - } else { - $public_key = \rtrim( $public_key ); } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { From 30b93a0d0779cb7129f4824ba25c626beed685e0 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Mon, 8 May 2023 13:20:38 +0200 Subject: [PATCH 130/427] General: add PR template This should help folks craft Pull Request descriptions that are as helpful as they can be. --- .github/PULL_REQUEST_TEMPLATE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..49ff40e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ + + +Fixes # + +## Proposed changes: + +* + +### Other information: + +- [ ] Have you written new tests for your changes, if applicable? + +## Testing instructions: + + + + + + +* Go to '..' +* + From 6d96daa635bd505df7843e69153a05eaaa40c4c5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 8 May 2023 21:05:20 +0200 Subject: [PATCH 131/427] fix NodeInfo check --- includes/class-activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 9739b6c..417aa29 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -280,7 +280,7 @@ class Activitypub { ); } - if ( ! \class_exists( 'Nodeinfo' ) || (bool) \get_option( 'blog_public', 1 ) ) { + if ( ! \class_exists( 'Nodeinfo' ) && true === (bool) \get_option( 'blog_public', 1 ) ) { \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', From f64a765129dadb137b541d5babab7cf36a542159 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 10:08:51 +0200 Subject: [PATCH 132/427] phpdoc fixes --- includes/collection/class-followers.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index c6c6223..1868bba 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -186,7 +186,7 @@ class Followers { } /** - * Handles "Unfollow" requests + * Handle "Unfollow" requests * * @param array $object The JSON "Undo" Activity * @param int $user_id The ID of the ID of the WordPress User @@ -202,7 +202,7 @@ class Followers { } /** - * Add a new Follower + * Add new Follower * * @param int $user_id The ID of the WordPress User * @param string $actor The Actor URL From 4abd5aefb45e651af0f31b328f47817de46999e8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 10:28:23 +0200 Subject: [PATCH 133/427] cache inbox list --- includes/collection/class-followers.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index c6c6223..a6a8ba4 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -19,6 +19,7 @@ use function Activitypub\get_remote_metadata_by_actor; */ class Followers { const TAXONOMY = 'activitypub-followers'; + const CACHE_KEY_INBOXES = 'activitypub_follower_inboxes_for_%s'; /** * Register WordPress hooks/actions and register Taxonomy @@ -225,6 +226,8 @@ class Followers { if ( is_wp_error( $result ) ) { return $result; } else { + $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); + wp_cache_delete( $cache_key ); return $follower; } } @@ -238,6 +241,8 @@ class Followers { * @return bool|WP_Error True on success, false or WP_Error on failure. */ public static function remove_follower( $user_id, $actor ) { + $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); + wp_cache_delete( $cache_key ); return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); } @@ -369,6 +374,13 @@ class Followers { * @return array The list of Inboxes */ public static function get_inboxes( $user_id ) { + $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); + $inboxes = wp_cache_get( $cache_key ); + + if ( $inboxes ) { + return $inboxes; + } + // get all Followers of a ID of the WordPress User $terms = new WP_Term_Query( array( @@ -402,6 +414,9 @@ class Followers { ) ); - return array_filter( $results ); + $inboxes = array_filter( $results ); + wp_cache_set( $cache_key, $inboxes ); + + return $inboxes; } } From 378f5dacdc305a6f83755549277500cd7fc817b0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:32:26 +0200 Subject: [PATCH 134/427] fix issue with missing array --- includes/class-signature.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c426727..a56a206 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -22,12 +22,12 @@ class Signature { } if ( -1 === $user_id ) { - $key = array( \get_option( 'activitypub_magic_sig_public_key' ) ); + $key = \get_option( 'activitypub_magic_sig_public_key' ); } else { - $key = \get_user_meta( $user_id, 'magic_sig_public_key' ); + $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); } - return $key[0]; + return $key; } /** @@ -43,10 +43,10 @@ class Signature { if ( -1 === $user_id ) { $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { - $key = \get_user_meta( $user_id, 'magic_sig_private_key' ); + $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); } - return $key[0]; + return $key; } /** @@ -69,10 +69,10 @@ class Signature { if ( -1 === $user_id ) { // private key - \add_option( 'activitypub_magic_sig_private_key', $priv_key ); + \update_option( 'activitypub_magic_sig_private_key', $priv_key ); // public key - \add_option( 'activitypub_magic_sig_public_key', $detail['key'] ); + \update_option( 'activitypub_magic_sig_public_key', $detail['key'] ); } else { // private key From c42edfce6825a3c913ae332561d17c3de8c55edb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:51:53 +0200 Subject: [PATCH 135/427] use WP_Error --- includes/class-signature.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a56a206..1032438 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -1,6 +1,7 @@ 403 ) ); + return new WP_Error( 'activitypub_signature', 'Request not signed', array( 'status' => 403 ) ); } if ( array_key_exists( 'signature', $headers ) ) { @@ -152,7 +153,7 @@ class Signature { } if ( ! isset( $signature_block ) || ! $signature_block ) { - return new \WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Incompatible request signature. keyId and signature are required', array( 'status' => 403 ) ); } $signed_headers = $signature_block['headers']; @@ -162,12 +163,12 @@ class Signature { $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); if ( ! $signed_data ) { - return new \WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Signed request date outside acceptable time window', array( 'status' => 403 ) ); } $algorithm = self::get_signature_algorithm( $signature_block ); if ( ! $algorithm ) { - return new \WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', array( 'status' => 403 ) ); } if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) { @@ -183,7 +184,7 @@ class Signature { } if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore - return new \WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Invalid Digest header', array( 'status' => 403 ) ); } } @@ -193,7 +194,7 @@ class Signature { } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; if ( ! $verified ) { - return new \WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); + return new WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } return $verified; } From 96953dfc7e033036941a2d423981dc506e3adda0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 11:57:43 +0200 Subject: [PATCH 136/427] fail early and always return $response as fallback --- includes/rest/class-server.php | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 0e8df43..8deac10 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -80,24 +80,28 @@ class Server { */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); - if ( str_starts_with( $route, '/activitypub' ) ) { - if ( 'POST' === $request->get_method() ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } - } else { - if ( '/activitypub/1.0/webfinger' !== $route ) { - // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } + if ( ! str_starts_with( $route, '/activitypub' ) ) { + return $response; + } + + if ( 'POST' === $request->get_method() ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; + } + } else { + if ( '/activitypub/1.0/webfinger' !== $route ) { + // SecureMode/Authorized fetch. + $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + if ( $secure_mode ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; } } } } + + return $response; } } From 180e882c4af2803f471ecba42beb3b2d3ea00c70 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:12:05 +0200 Subject: [PATCH 137/427] generate key if not existent --- includes/class-signature.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 1032438..796cb2c 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -28,6 +28,10 @@ class Signature { $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); } + if ( ! $key ) { + return self::generate_key_pair( $user_id, true ); + } + return $key; } @@ -47,6 +51,10 @@ class Signature { $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); } + if ( ! $key ) { + return self::generate_key_pair( $user_id, true ); + } + return $key; } @@ -108,6 +116,8 @@ class Signature { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } + var_dump($key); + $signature = null; \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore From c872cb69d08aab49f2ab30856fc3112814da7f2d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:13:35 +0200 Subject: [PATCH 138/427] remove var_dump :( --- includes/class-signature.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 796cb2c..16e33ea 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -116,8 +116,6 @@ class Signature { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } - var_dump($key); - $signature = null; \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore From b88c5f606dabbe1cb43e31541e9509ad273fdc2a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:17:48 +0200 Subject: [PATCH 139/427] fixed copy/paste issue --- includes/class-signature.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 16e33ea..c958379 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -29,7 +29,7 @@ class Signature { } if ( ! $key ) { - return self::generate_key_pair( $user_id, true ); + return self::get_public_key( $user_id, true ); } return $key; @@ -52,7 +52,7 @@ class Signature { } if ( ! $key ) { - return self::generate_key_pair( $user_id, true ); + return self::get_private_key( $user_id, true ); } return $key; From ca8aff182350b57561f5868c583bd7d6bc326cd4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 12:25:25 +0200 Subject: [PATCH 140/427] cast to bool, to be sure that '0' is false --- includes/class-activitypub.php | 2 +- includes/rest/class-server.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index c0695a9..6e79fa6 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,7 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); if ( $secure_mode ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 8deac10..712a3af 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -92,7 +92,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - $secure_mode = \get_option( 'activitypub_use_secure_mode', '0' ); + $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); if ( $secure_mode ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { From 655227058e86a10675510782f57f055708c7bc3f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 9 May 2023 13:02:30 +0200 Subject: [PATCH 141/427] remove `

` because of autop --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 5c3ff4a..2e731f2 100644 --- a/activitypub.php +++ b/activitypub.php @@ -24,7 +24,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

[ap_title]

\n\n[ap_content]\n\n

[ap_hashtags]

\n\n

[ap_shortlink]

" ); + \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); From 74be5d6b5159bed33b67e377a7fbf6383d66fee1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 09:04:33 +0200 Subject: [PATCH 142/427] implemented feedback of @akirk --- includes/collection/class-followers.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index a6a8ba4..5e5e532 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -19,7 +19,7 @@ use function Activitypub\get_remote_metadata_by_actor; */ class Followers { const TAXONOMY = 'activitypub-followers'; - const CACHE_KEY_INBOXES = 'activitypub_follower_inboxes_for_%s'; + const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; /** * Register WordPress hooks/actions and register Taxonomy @@ -226,8 +226,7 @@ class Followers { if ( is_wp_error( $result ) ) { return $result; } else { - $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); - wp_cache_delete( $cache_key ); + wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); return $follower; } } @@ -241,8 +240,7 @@ class Followers { * @return bool|WP_Error True on success, false or WP_Error on failure. */ public static function remove_follower( $user_id, $actor ) { - $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); - wp_cache_delete( $cache_key ); + wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); } @@ -375,7 +373,7 @@ class Followers { */ public static function get_inboxes( $user_id ) { $cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id ); - $inboxes = wp_cache_get( $cache_key ); + $inboxes = wp_cache_get( $cache_key, 'activitypub' ); if ( $inboxes ) { return $inboxes; @@ -415,7 +413,7 @@ class Followers { ); $inboxes = array_filter( $results ); - wp_cache_set( $cache_key, $inboxes ); + wp_cache_set( $cache_key, $inboxes, 'activitypub' ); return $inboxes; } From 17b66cb23d88b2ad0cbaf75606bf062f33a5d5e8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 14:18:56 +0200 Subject: [PATCH 143/427] implement `cleanup_followers` and `update_followers` --- activitypub.php | 1 + includes/class-activitypub.php | 54 +------- includes/class-migration.php | 10 +- includes/class-scheduler.php | 126 ++++++++++++++++++ includes/class-shortcodes.php | 8 +- includes/collection/class-followers.php | 78 +++++++++++ includes/model/class-follower.php | 8 ++ tests/test-class-db-activitypub-followers.php | 54 ++++++++ 8 files changed, 281 insertions(+), 58 deletions(-) create mode 100644 includes/class-scheduler.php diff --git a/activitypub.php b/activitypub.php index 0db3c32..4fbe2fe 100644 --- a/activitypub.php +++ b/activitypub.php @@ -49,6 +49,7 @@ function init() { Mention::init(); Debug::init(); Health_Check::init(); + Scheduler::init(); } \add_action( 'plugins_loaded', '\Activitypub\init' ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 417aa29..37bedf4 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -22,7 +22,6 @@ class Activitypub { \add_post_type_support( $post_type, 'activitypub' ); } - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 ); \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); @@ -37,13 +36,7 @@ class Activitypub { public static function activate() { self::flush_rewrite_rules(); - if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) { - \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' ); - } - - if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) { - \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' ); - } + Scheduler::register_schedules(); } /** @@ -54,8 +47,7 @@ class Activitypub { public static function deactivate() { self::flush_rewrite_rules(); - wp_unschedule_hook( 'activitypub_update_followers' ); - wp_unschedule_hook( 'activitypub_cleanup_followers' ); + Scheduler::deregister_schedules(); } /** @@ -137,48 +129,6 @@ class Activitypub { return $vars; } - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $activitypub_post = new \Activitypub\Model\Post( $post ); - - if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_create_activity', - array( $activitypub_post ) - ); - } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_activity', - array( $activitypub_post ) - ); - } elseif ( 'trash' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_delete_activity', - array( $activitypub_post ) - ); - } - } - /** * Replaces the default avatar. * diff --git a/includes/class-migration.php b/includes/class-migration.php index 1e8c44b..e6e6fba 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -3,7 +3,15 @@ namespace Activitypub; use Acctivitypub\Model\Follower; +/** + * ActivityPub Migration Class + * + * @author Matthias Pfefferle + */ class Migration { + /** + * Initialize the class, registering WordPress hooks + */ public static function init() { \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); } @@ -59,7 +67,7 @@ class Migration { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followers ) { - foreach ( $followers as $follower ) { + foreach ( $followers as $actor ) { $meta = get_remote_metadata_by_actor( $actor ); $follower = new Follower( $actor ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php new file mode 100644 index 0000000..4291365 --- /dev/null +++ b/includes/class-scheduler.php @@ -0,0 +1,126 @@ +post_type, $post_types, true ) ) { + return; + } + + $activitypub_post = new Post( $post ); + + if ( 'publish' === $new_status && 'publish' !== $old_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_create_activity', + array( $activitypub_post ) + ); + } elseif ( 'publish' === $new_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_update_activity', + array( $activitypub_post ) + ); + } elseif ( 'trash' === $new_status ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_delete_activity', + array( $activitypub_post ) + ); + } + } + + /** + * Update followers + * + * @return void + */ + public static function update_followers() { + $followers = Followers::get_outdated_followers( ACTIVITYPUB_OBJECT ); + + foreach ( $followers as $follower ) { + $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_error( $meta ); + } else { + $follower->from_meta( $meta ); + } + + $follower->update(); + } + } + + /** + * Cleanup followers + * + * @return void + */ + public static function cleanup_followers() { + $followers = Followers::get_faulty_followers( ACTIVITYPUB_OBJECT ); + + foreach ( $followers as $follower ) { + $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + if ( 5 >= $follower->count_errors() ) { + $follower->delete(); + } else { + $follower->set_error( $meta ); + $follower->update(); + } + } else { + $follower->reset_errors(); + } + } + } +} diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 7289808..f56d483 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -3,14 +3,12 @@ namespace Activitypub; class Shortcodes { /** - * Class constructor, registering WordPress then shortcodes - * - * @param WP_Post $post A WordPress Post Object + * Initialize the class, registering WordPress hooks */ public static function init() { - foreach ( get_class_methods( 'Activitypub\Shortcodes' ) as $shortcode ) { + foreach ( get_class_methods( self::class ) as $shortcode ) { if ( 'init' !== $shortcode ) { - add_shortcode( 'ap_' . $shortcode, array( 'Activitypub\Shortcodes', $shortcode ) ); + add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) ); } } } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 1868bba..0fa4a6a 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -404,4 +404,82 @@ class Followers { return array_filter( $results ); } + + /** + * Undocumented function + * + * @return void + */ + public static function get_outdated_followers( $output = ARRAY_N, $number = 50, $older_than = 604800 ) { + $args = array( + 'taxonomy' => self::TAXONOMY, + 'number' => $number, + 'meta_key' => 'updated_at', + 'orderby' => 'meta_value_num', + 'order' => 'DESC', + 'meta_query' => array( + array( + 'key' => 'updated_at', + 'value' => strtotime( 'now' ) - $older_than, + 'type' => 'numeric', + 'compare' => '<=', + ), + ), + ); + + $terms = new WP_Term_Query( $args ); + + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } + } + + public static function get_faulty_followers( $output = ARRAY_N, $number = 10 ) { + $args = array( + 'taxonomy' => self::TAXONOMY, + 'number' => $number, + 'meta_query' => array( + array( + 'key' => 'errors', + 'compare' => 'EXISTS', + ), + ), + ); + + $terms = new WP_Term_Query( $args ); + + $items = array(); + + // change output format + switch ( $output ) { + case ACTIVITYPUB_OBJECT: + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore + } + return $items; + case OBJECT: + return $terms->get_terms(); + case ARRAY_N: + default: + foreach ( $terms->get_terms() as $follower ) { + $items[] = $follower->name; // phpcs:ignore + } + return $items; + } + } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 3a15690..2b152f1 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -210,6 +210,10 @@ class Follower { return $this->errors; } + public function reset_errors() { + delete_term_meta( $this->id, 'errors' ); + } + public function count_errors() { $errors = $this->get_errors(); @@ -292,6 +296,10 @@ class Follower { } } + public function delete() { + wp_delete_term( $this->id, Followers::TAXONOMY ); + } + protected function update_term_meta() { $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 640c4d1..cf8b53f 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -90,6 +90,60 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); } + public function test_get_outdated_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); + + update_term_meta( $follower->get_id(), 'updated_at', strtotime( 'now' ) - 804800 ); + + $followers = \Activitypub\Collection\Followers::get_outdated_followers(); + $this->assertEquals( 1, count( $followers ) ); + $this->assertEquals( 'https://example.com/author/jon', $followers[0] ); + } + + public function test_get_faulty_followers() { + $followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + } + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + + for ( $i = 1; $i <= 15; $i++ ) { + add_term_meta( $follower->get_id(), 'errors', 'error ' . $i ); + } + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $count = $follower->count_errors(); + + $followers = \Activitypub\Collection\Followers::get_faulty_followers(); + + $this->assertEquals( 1, count( $followers ) ); + $this->assertEquals( 'http://sally.example.org', $followers[0] ); + + $follower->reset_errors(); + + $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $count = $follower->count_errors(); + + $followers = \Activitypub\Collection\Followers::get_faulty_followers(); + + $this->assertEquals( 0, count( $followers ) ); + } + + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { return true; From 2570928b00d9c6ff8d05b427f45a1a76684a51e9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 14:55:10 +0200 Subject: [PATCH 144/427] PHPDoc --- includes/class-scheduler.php | 10 ++++++++++ includes/collection/class-followers.php | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 4291365..0373c3f 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -21,6 +21,11 @@ class Scheduler { \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); } + /** + * Schedule all ActivityPub schedules. + * + * @return void + */ public static function register_schedules() { if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) { \wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' ); @@ -31,6 +36,11 @@ class Scheduler { } } + /** + * Unscedule all ActivityPub schedules. + * + * @return void + */ public static function deregister_schedules() { wp_unschedule_hook( 'activitypub_update_followers' ); wp_unschedule_hook( 'activitypub_cleanup_followers' ); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 0fa4a6a..1d54fa2 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -406,9 +406,13 @@ class Followers { } /** - * Undocumented function + * Get all Followers that have not been updated for a given time * - * @return void + * @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT. + * @param int $number Limits the result. + * @param int $older_than The time in seconds. + * + * @return mixed The Term list of Followers, the format depends on $output. */ public static function get_outdated_followers( $output = ARRAY_N, $number = 50, $older_than = 604800 ) { $args = array( @@ -449,6 +453,14 @@ class Followers { } } + /** + * Get all Followers that had errors + * + * @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT + * @param integer $number The number of Followers to return. + * + * @return mixed The Term list of Followers, the format depends on $output. + */ public static function get_faulty_followers( $output = ARRAY_N, $number = 10 ) { $args = array( 'taxonomy' => self::TAXONOMY, From df02d2202e0492efee9acbc028dfe0135db00cd0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 15:02:01 +0200 Subject: [PATCH 145/427] PHPDoc --- includes/model/class-follower.php | 76 +++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 2b152f1..10200c2 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -139,8 +139,8 @@ class Follower { /** * Magic function to implement getter and setter * - * @param string $method - * @param string $params + * @param string $method The method name. + * @param string $params The method params. * * @return void */ @@ -159,6 +159,13 @@ class Follower { } } + /** + * Prefill the Object with the meta data. + * + * @param array $meta The meta data. + * + * @return void + */ public function from_meta( $meta ) { $this->meta = $meta; @@ -181,6 +188,13 @@ class Follower { $this->updated_at = \strtotime( 'now' ); } + /** + * Get the data by the given attribute + * + * @param string $attribute The attribute name. + * + * @return mixed The attribute value. + */ public function get( $attribute ) { if ( $this->$attribute ) { return $this->$attribute; @@ -201,6 +215,11 @@ class Follower { return null; } + /** + * Get the errors. + * + * @return mixed + */ public function get_errors() { if ( $this->errors ) { return $this->errors; @@ -210,10 +229,20 @@ class Follower { return $this->errors; } + /** + * Reset (delete) all errors. + * + * @return void + */ public function reset_errors() { delete_term_meta( $this->id, 'errors' ); } + /** + * Count the errors. + * + * @return int The number of errors. + */ public function count_errors() { $errors = $this->get_errors(); @@ -224,6 +253,11 @@ class Follower { return 0; } + /** + * Return the latest error message. + * + * @return string The error message. + */ public function get_latest_error_message() { $errors = $this->get_errors(); @@ -234,6 +268,13 @@ class Follower { return ''; } + /** + * Get the meta data by the given attribute. + * + * @param string $attribute The attribute name. + * + * @return mixed $attribute The attribute value. + */ public function get_meta_by( $attribute ) { $meta = $this->get_meta(); @@ -252,6 +293,11 @@ class Follower { return null; } + /** + * Get the meta data. + * + * @return array $meta The meta data. + */ public function get_meta() { if ( $this->meta ) { return $this->meta; @@ -260,6 +306,11 @@ class Follower { return null; } + /** + * Update the current Follower-Object. + * + * @return void + */ public function update() { $term = wp_update_term( $this->id, @@ -273,6 +324,11 @@ class Follower { $this->update_term_meta(); } + /** + * Save the current Follower-Object. + * + * @return void + */ public function save() { $term = wp_insert_term( $this->actor, @@ -288,6 +344,11 @@ class Follower { $this->update_term_meta(); } + /** + * Upsert the current Follower-Object. + * + * @return void + */ public function upsert() { if ( $this->id ) { $this->update(); @@ -296,10 +357,20 @@ class Follower { } } + /** + * Delete the current Follower-Object. + * + * @return void + */ public function delete() { wp_delete_term( $this->id, Followers::TAXONOMY ); } + /** + * Update the term meta. + * + * @return void + */ protected function update_term_meta() { $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); @@ -320,6 +391,5 @@ class Follower { add_term_meta( $this->id, 'errors', $error ); } - } } From 154b0018afd2b4407c692379c75e3e4b188d487f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 15:36:45 +0200 Subject: [PATCH 146/427] PHPDoc --- includes/class-activity-dispatcher.php | 16 ++--- includes/debug.php | 3 +- includes/functions.php | 40 ++++++------ includes/model/class-activity.php | 85 +++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 31 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 422240d..ee435a7 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -5,6 +5,8 @@ use Activitypub\Model\Post; use Activitypub\Model\Activity; use Activitypub\Collection\Followers; +use function Activitypub\safe_remote_post; + /** * ActivityPub Activity_Dispatcher Class * @@ -37,7 +39,7 @@ class Activity_Dispatcher { /** * Send "update" activities. * - * @param Activitypub\Model\Post $activitypub_post + * @param Activitypub\Model\Post $activitypub_post The ActivityPub Post. */ public static function send_update_activity( Post $activitypub_post ) { self::send_activity( $activitypub_post, 'Update' ); @@ -46,23 +48,23 @@ class Activity_Dispatcher { /** * Send "delete" activities. * - * @param Activitypub\Model\Post $activitypub_post + * @param Activitypub\Model\Post $activitypub_post The ActivityPub Post. */ public static function send_delete_activity( Post $activitypub_post ) { self::send_activity( $activitypub_post, 'Delete' ); } /** - * Undocumented function + * Send Activities to followers and mentioned users. * - * @param Activitypub\Model\Post $activitypub_post - * @param [type] $activity_type + * @param Activitypub\Model\Post $activitypub_post The ActivityPub Post. + * @param string $activity_type The Activity-Type. * * @return void */ public static function send_activity( Post $activitypub_post, $activity_type ) { // check if a migration is needed before sending new posts - \Activitypub\Migration::maybe_migrate(); + Migration::maybe_migrate(); // get latest version of post $user_id = $activitypub_post->get_post_author(); @@ -79,7 +81,7 @@ class Activity_Dispatcher { foreach ( $inboxes as $inbox ) { $activity = $activitypub_activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, $user_id ); + safe_remote_post( $inbox, $activity, $user_id ); } } } diff --git a/includes/debug.php b/includes/debug.php index a5683f4..d42b2a9 100644 --- a/includes/debug.php +++ b/includes/debug.php @@ -6,7 +6,8 @@ namespace Activitypub; * * @param array $r Array of HTTP request args. * @param string $url The request URL. - * @return array $args Array or string of HTTP request arguments. + * + * @return array Array or string of HTTP request arguments. */ function allow_localhost( $r, $url ) { $r['reject_unsafe_urls'] = false; diff --git a/includes/functions.php b/includes/functions.php index bd04e6c..19a3512 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -43,9 +43,9 @@ function safe_remote_get( $url, $user_id ) { /** * Returns a users WebFinger "resource" * - * @param int $user_id + * @param int $user_id The User-ID. * - * @return string The user-resource + * @return string The User-Resource. */ function get_webfinger_resource( $user_id ) { return Webfinger::get_user_resource( $user_id ); @@ -123,29 +123,24 @@ function get_remote_metadata_by_actor( $actor ) { return $metadata; } -function get_identifier_settings( $user_id ) { - ?> - - - - - - - -
- - -

or

- -

-
- context = null; @@ -47,6 +100,14 @@ class Activity { $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) ); } + /** + * Magic Getter/Setter + * + * @param string $method The method name. + * @param string $params The method params. + * + * @return mixed The value. + */ public function __call( $method, $params ) { $var = \strtolower( \substr( $method, 4 ) ); @@ -73,6 +134,13 @@ class Activity { } } + /** + * Convert from a Post-Object. + * + * @param Post $post The Post-Object. + * + * @return void + */ public function from_post( Post $post ) { $this->object = $post->to_array(); @@ -111,6 +179,11 @@ class Activity { } + /** + * Convert to an Array. + * + * @return array The Array. + */ public function to_array() { $array = array_filter( \get_object_vars( $this ) ); @@ -126,12 +199,17 @@ class Activity { /** * Convert to JSON * - * @return void + * @return string The JSON. */ public function to_json() { return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } + /** + * Convert to a Simple Array. + * + * @return string The array. + */ public function to_simple_array() { $activity = array( '@context' => $this->context, @@ -149,6 +227,11 @@ class Activity { return $activity; } + /** + * Convert to a Simple JSON. + * + * @return string The JSON. + */ public function to_simple_json() { return \wp_json_encode( $this->to_simple_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } From 463bff834b3c845ff6ab10acdad51c9d37f29cfe Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 17:21:59 +0200 Subject: [PATCH 147/427] delete if response code is 410 or 404 --- includes/class-scheduler.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 0373c3f..0a7bba7 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -121,7 +121,9 @@ class Scheduler { foreach ( $followers as $follower ) { $meta = get_remote_metadata_by_actor( $follower->get_actor() ); - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + if ( is_tombstone( $meta ) ) { + $follower->delete(); + } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { if ( 5 >= $follower->count_errors() ) { $follower->delete(); } else { From 9da559be6aee7ae2fca88122b15dc05a899f0ad1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:45:32 +0200 Subject: [PATCH 148/427] Update includes/collection/class-followers.php Co-authored-by: Alex Kirk --- includes/collection/class-followers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 1d54fa2..0948bfb 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -424,7 +424,7 @@ class Followers { 'meta_query' => array( array( 'key' => 'updated_at', - 'value' => strtotime( 'now' ) - $older_than, + 'value' => time() - $older_than, 'type' => 'numeric', 'compare' => '<=', ), From 3c027449257186cbe3306cf92584349b283fa410 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:45:48 +0200 Subject: [PATCH 149/427] Update activitypub.php Co-authored-by: Alex Kirk --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 4fbe2fe..598f03a 100644 --- a/activitypub.php +++ b/activitypub.php @@ -66,7 +66,7 @@ spl_autoload_register( if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) { $class = str_replace( 'activitypub\\', '', $class ); - if ( strpos( $class, '\\' ) ) { + if ( false !== strpos( $class, '\\' ) ) { list( $sub_dir, $class ) = explode( '\\', $class ); $base_dir = $base_dir . $sub_dir . DIRECTORY_SEPARATOR; } From 6fce2c30d2416159e98fe08f6b05b52ee1ed93a2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:47:46 +0200 Subject: [PATCH 150/427] Update includes/class-scheduler.php Co-authored-by: Alex Kirk --- includes/class-scheduler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 0a7bba7..4bfb8b8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -124,7 +124,7 @@ class Scheduler { if ( is_tombstone( $meta ) ) { $follower->delete(); } elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - if ( 5 >= $follower->count_errors() ) { + if ( 5 <= $follower->count_errors() ) { $follower->delete(); } else { $follower->set_error( $meta ); From 75c1c6a402c605cac157cd95582e43004a898350 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:50:20 +0200 Subject: [PATCH 151/427] Update activitypub.php Co-authored-by: Alex Kirk --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 598f03a..4d9b2e6 100644 --- a/activitypub.php +++ b/activitypub.php @@ -71,7 +71,7 @@ spl_autoload_register( $base_dir = $base_dir . $sub_dir . DIRECTORY_SEPARATOR; } - $filename = 'class-' . str_replace( '_', '-', $class ); + $filename = 'class-' . strtr( $class, '_', '-' ); $file = $base_dir . $filename . '.php'; if ( file_exists( $file ) ) { From 26a1dc9be5165b40369fba9acc36afd2ee1a4db1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:52:13 +0200 Subject: [PATCH 152/427] use time() instead of strtotime( 'now' ) --- includes/collection/class-followers.php | 2 +- includes/model/class-activity.php | 2 +- includes/model/class-follower.php | 4 ++-- tests/test-class-db-activitypub-followers.php | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 0948bfb..413af3b 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -143,7 +143,7 @@ class Followers { 'single' => true, 'sanitize_callback' => function( $value ) { if ( ! is_numeric( $value ) && (int) $value !== $value ) { - $value = strtotime( 'now' ); + $value = \time(); } return $value; diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 34f6146..6a8d45a 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -44,7 +44,7 @@ class Activity { } $this->type = \ucfirst( $type ); - $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) ); + $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \time() ); } public function __call( $method, $params ) { diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 10200c2..0668e4d 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -185,7 +185,7 @@ class Follower { $this->shared_inbox = $meta['inbox']; } - $this->updated_at = \strtotime( 'now' ); + $this->updated_at = \time(); } /** @@ -320,7 +320,7 @@ class Follower { ) ); - $this->updated_at = \strtotime( 'now' ); + $this->updated_at = \time(); $this->update_term_meta(); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index cf8b53f..2467363 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -102,7 +102,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); - update_term_meta( $follower->get_id(), 'updated_at', strtotime( 'now' ) - 804800 ); + update_term_meta( $follower->get_id(), 'updated_at', \time() - 804800 ); $followers = \Activitypub\Collection\Followers::get_outdated_followers(); $this->assertEquals( 1, count( $followers ) ); From baa8027e3fe5e691b6197ab45fc6c07b10f48838 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 10 May 2023 18:53:09 +0200 Subject: [PATCH 153/427] check if file is_readable thanks @akirk --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 4d9b2e6..117b5cb 100644 --- a/activitypub.php +++ b/activitypub.php @@ -74,7 +74,7 @@ spl_autoload_register( $filename = 'class-' . strtr( $class, '_', '-' ); $file = $base_dir . $filename . '.php'; - if ( file_exists( $file ) ) { + if ( file_exists( $file ) && is_readable( $file ) ) { require_once $file; } } From fc1b89561e7f6e07a2e726db6d5f8d265bcac92e Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 10 May 2023 19:46:52 -0600 Subject: [PATCH 154/427] If WP_REST_Request set actor for get_remote_key() --- includes/class-signature.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index c958379..7c948f9 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -143,10 +143,12 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); + $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array + $actor = null; $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; } @@ -196,7 +198,11 @@ class Signature { } } - $public_key = self::get_remote_key( $signature_block['keyId'] ); + if ( $actor ) { + $public_key = self::get_remote_key( $actor ); + } else { + $public_key = self::get_remote_key( $signature_block['keyId'] ); + } if ( \is_wp_error( $public_key ) ) { return $public_key; } From 7b545b4639e3af5d49c4c432b98de2f8459460e1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 09:09:13 +0200 Subject: [PATCH 155/427] remove DIRECTORY_SEPARATOR because its not used anywhere else --- activitypub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 117b5cb..0528965 100644 --- a/activitypub.php +++ b/activitypub.php @@ -58,7 +58,7 @@ function init() { */ spl_autoload_register( function ( $class ) { - $base_dir = trailingslashit( __DIR__ ) . 'includes' . DIRECTORY_SEPARATOR; + $base_dir = \dirname( __FILE__ ) . '/includes/'; $base = 'activitypub'; $class = strtolower( $class ); @@ -68,7 +68,7 @@ spl_autoload_register( if ( false !== strpos( $class, '\\' ) ) { list( $sub_dir, $class ) = explode( '\\', $class ); - $base_dir = $base_dir . $sub_dir . DIRECTORY_SEPARATOR; + $base_dir = $base_dir . $sub_dir . '/'; } $filename = 'class-' . strtr( $class, '_', '-' ); From 47b1b10955fec4118c3149553480fb1e4dc45110 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 09:45:01 +0200 Subject: [PATCH 156/427] Fix migration script --- includes/class-migration.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index e6e6fba..7e7cd9a 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,7 +1,8 @@ upsert(); - $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); + $result = wp_set_object_terms( $user_id, $follower->get_actor(), Followers::TAXONOMY, true ); } } } From b8039141809982567028a85a79914a77cee70ba8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 09:46:26 +0200 Subject: [PATCH 157/427] removed output formatting --- activitypub.php | 2 - includes/class-scheduler.php | 4 +- includes/collection/class-followers.php | 66 +++++-------------- includes/model/class-follower.php | 9 +++ includes/rest/class-followers.php | 8 ++- includes/table/class-followers.php | 2 +- tests/test-class-db-activitypub-followers.php | 7 ++ 7 files changed, 41 insertions(+), 57 deletions(-) diff --git a/activitypub.php b/activitypub.php index 0528965..47e4167 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,8 +29,6 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); - \define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' ); - Migration::init(); Activity_Dispatcher::init(); Activitypub::init(); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 4bfb8b8..a79044e 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -95,7 +95,7 @@ class Scheduler { * @return void */ public static function update_followers() { - $followers = Followers::get_outdated_followers( ACTIVITYPUB_OBJECT ); + $followers = Followers::get_outdated_followers(); foreach ( $followers as $follower ) { $meta = get_remote_metadata_by_actor( $follower->get_actor() ); @@ -116,7 +116,7 @@ class Scheduler { * @return void */ public static function cleanup_followers() { - $followers = Followers::get_faulty_followers( ACTIVITYPUB_OBJECT ); + $followers = Followers::get_faulty_followers(); foreach ( $followers as $follower ) { $meta = get_remote_metadata_by_actor( $follower->get_actor() ); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 413af3b..8a271f6 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -316,7 +316,7 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) { + public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { $defaults = array( 'taxonomy' => self::TAXONOMY, 'hide_empty' => false, @@ -329,25 +329,13 @@ class Followers { $args = wp_parse_args( $args, $defaults ); $terms = new WP_Term_Query( $args ); - $items = array(); - // change output format - switch ( $output ) { - case ACTIVITYPUB_OBJECT: - foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore - } - return $items; - case OBJECT: - return $terms->get_terms(); - case ARRAY_N: - default: - foreach ( $terms->get_terms() as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore } + + return $items; } /** @@ -414,7 +402,7 @@ class Followers { * * @return mixed The Term list of Followers, the format depends on $output. */ - public static function get_outdated_followers( $output = ARRAY_N, $number = 50, $older_than = 604800 ) { + public static function get_outdated_followers( $number = 50, $older_than = 604800 ) { $args = array( 'taxonomy' => self::TAXONOMY, 'number' => $number, @@ -432,25 +420,13 @@ class Followers { ); $terms = new WP_Term_Query( $args ); - $items = array(); - // change output format - switch ( $output ) { - case ACTIVITYPUB_OBJECT: - foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore - } - return $items; - case OBJECT: - return $terms->get_terms(); - case ARRAY_N: - default: - foreach ( $terms->get_terms() as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore } + + return $items; } /** @@ -461,7 +437,7 @@ class Followers { * * @return mixed The Term list of Followers, the format depends on $output. */ - public static function get_faulty_followers( $output = ARRAY_N, $number = 10 ) { + public static function get_faulty_followers( $number = 10 ) { $args = array( 'taxonomy' => self::TAXONOMY, 'number' => $number, @@ -474,24 +450,12 @@ class Followers { ); $terms = new WP_Term_Query( $args ); - $items = array(); - // change output format - switch ( $output ) { - case ACTIVITYPUB_OBJECT: - foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore - } - return $items; - case OBJECT: - return $terms->get_terms(); - case ARRAY_N: - default: - foreach ( $terms->get_terms() as $follower ) { - $items[] = $follower->name; // phpcs:ignore - } - return $items; + foreach ( $terms->get_terms() as $follower ) { + $items[] = new Follower( $follower->name ); // phpcs:ignore } + + return $items; } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 0668e4d..6c03fd8 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -159,6 +159,15 @@ class Follower { } } + /** + * Magic function to return the Actor-URL when the Object is used as a string + * + * @return string + */ + public function __toString() { + return $this->get_actor(); + } + /** * Prefill the Object with the meta data. * diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 9a8b9b6..7dd49a8 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -81,7 +81,13 @@ class Followers { $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore - $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore + // phpcs:ignore + $json->orderedItems = array_map( + function( $item ) { + return $item->get_actor(); + }, + FollowerCollection::get_followers( $user_id ) + ); $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index c9b5948..03fb70f 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -35,7 +35,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page ); + $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); $counter = FollowerCollection::count_followers( \get_current_user_id() ); $this->items = array(); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 2467363..a04145a 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -58,6 +58,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertEquals( 3, \count( $db_followers ) ); + $db_followers = array_map( + function( $item ) { + return $item->get_actor(); + }, + $db_followers + ); + $this->assertSame( array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ), $db_followers ); } From b85b0167c0e9acf44d42f8c3b109cfb1246f7b0b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 10:53:19 +0200 Subject: [PATCH 158/427] Update activitypub.php Co-authored-by: Alex Kirk --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 47e4167..cb67fa8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -82,7 +82,7 @@ spl_autoload_register( require_once \dirname( __FILE__ ) . '/includes/functions.php'; // load NodeInfo endpoints only if blog is public -if ( true === (bool) \get_option( 'blog_public', 1 ) ) { +if ( \get_option( 'blog_public', 1 ) ) { Rest\NodeInfo::init(); } From f196047901e606f07eb4bcbe3e1adb31b0c16cbf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 11:02:06 +0200 Subject: [PATCH 159/427] remove casts after feedback from @akirk --- activitypub.php | 2 +- includes/class-activitypub.php | 3 +-- includes/rest/class-server.php | 3 +-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index da2eea2..049d7d7 100644 --- a/activitypub.php +++ b/activitypub.php @@ -75,7 +75,7 @@ function init() { Rest\Webfinger::init(); // load NodeInfo endpoints only if blog is public - if ( true === (bool) \get_option( 'blog_public', 1 ) ) { + if ( \get_option( 'blog_public', 1 ) ) { require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php'; Rest\NodeInfo::init(); } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6e79fa6..30cc7d0 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,8 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { + if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { // fallback as template_loader can't return http headers diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 712a3af..a5219f3 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -92,8 +92,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - $secure_mode = (bool) \get_option( 'activitypub_use_secure_mode', '0' ); - if ( $secure_mode ) { + if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; From 77873d12b3ed55bf66536d081a2a0f20a52009ec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 14:20:35 +0200 Subject: [PATCH 160/427] sanitize output --- includes/class-shortcodes.php | 44 +++++++++++++++-------------------- includes/class-webfinger.php | 2 +- includes/help.php | 4 ++-- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 7289808..f0f8e22 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -42,8 +42,8 @@ class Shortcodes { foreach ( $tags as $tag ) { $hash_tags[] = \sprintf( '', - \get_tag_link( $tag ), - $tag->slug + \esc_url( \get_tag_link( $tag ) ), + \esc_html( $tag->slug ) ); } @@ -66,7 +66,7 @@ class Shortcodes { return ''; } - return \get_the_title( $post_id ); + return \esc_html( \get_the_title( $post_id ) ); } @@ -170,7 +170,7 @@ class Shortcodes { } } - return \apply_filters( 'the_excerpt', $excerpt ); + return $excerpt; } /** @@ -189,21 +189,11 @@ class Shortcodes { return ''; } - $atts = shortcode_atts( - array( 'apply_filters' => 'yes' ), - $atts, - $tag - ); - $content = \get_post_field( 'post_content', $post ); - if ( 'yes' === $atts['apply_filters'] ) { - $content = \apply_filters( 'the_content', $content ); - } else { - $content = do_blocks( $content ); - $content = wptexturize( $content ); - $content = wp_filter_content_tags( $content ); - } + $content = do_blocks( $content ); + $content = wptexturize( $content ); + $content = wp_filter_content_tags( $content ); // replace script and style elements $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); @@ -343,7 +333,11 @@ class Shortcodes { $hash_tags = array(); foreach ( $categories as $category ) { - $hash_tags[] = \sprintf( '', \get_category_link( $category ), $category->slug ); + $hash_tags[] = \sprintf( + '', + \esc_url( \get_category_link( $category ) ), + \esc_html( $category->slug ) + ); } return \implode( ' ', $hash_tags ); @@ -365,13 +359,13 @@ class Shortcodes { return ''; } - $name = \get_the_author_meta( 'display_name', $post->post_author ); + $name = \esc_html( \get_the_author_meta( 'display_name', $post->post_author ) ); if ( ! $name ) { return ''; } - return $name; + return \esc_html( $name ); } /** @@ -422,7 +416,7 @@ class Shortcodes { * @return string */ public static function blogname( $atts, $content, $tag ) { - return \get_bloginfo( 'name' ); + return \esc_html( \get_bloginfo( 'name' ) ); } /** @@ -435,7 +429,7 @@ class Shortcodes { * @return string */ public static function blogdesc( $atts, $content, $tag ) { - return \get_bloginfo( 'description' ); + return \esc_html( \get_bloginfo( 'description' ) ); } /** @@ -464,7 +458,7 @@ class Shortcodes { return ''; } - return $date; + return \esc_html( $date ); } /** @@ -493,7 +487,7 @@ class Shortcodes { return ''; } - return $date; + return \esc_html( $date ); } /** @@ -522,6 +516,6 @@ class Shortcodes { return ''; } - return $date; + return \esc_html( $date ); } } diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 1581853..679f8a1 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -51,7 +51,7 @@ class Webfinger { } // try to access author URL - $response = \wp_remote_get( + $response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ), diff --git a/includes/help.php b/includes/help.php index 58339fb..ccf61cc 100644 --- a/includes/help.php +++ b/includes/help.php @@ -9,8 +9,8 @@ '
' . '
[ap_title]
' . '
' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), 'default' ) . '
' . - '
[ap_content apply_filters="yes"]
' . - '
' . \wp_kses( __( 'The post\'s content. With apply_filters you can decide if filters should be applied or not (default is yes). The values can be yes or no. apply_filters attribute is optional.', 'activitypub' ), 'default' ) . '
' . + '
[ap_content]
' . + '
' . \wp_kses( __( 'The post\'s content.', 'activitypub' ), 'default' ) . '
' . '
[ap_excerpt lenght="400"]
' . '
' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). length attribute is optional.', 'activitypub' ), 'default' ) . '
' . '
[ap_permalink type="url"]
' . From cfa8974ffaf862b9479f61d0c2c4348011835cd4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 14:38:57 +0200 Subject: [PATCH 161/427] support more more depth in the namespaces --- activitypub.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index cb67fa8..3e2d98f 100644 --- a/activitypub.php +++ b/activitypub.php @@ -65,7 +65,9 @@ spl_autoload_register( $class = str_replace( 'activitypub\\', '', $class ); if ( false !== strpos( $class, '\\' ) ) { - list( $sub_dir, $class ) = explode( '\\', $class ); + $parts = explode( '\\', $class ); + $class = array_pop( $parts ); + $sub_dir = implode( '/', $parts ); $base_dir = $base_dir . $sub_dir . '/'; } From 663c6315c96ec462ac272214f1b2c277d23ad542 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 14:40:47 +0200 Subject: [PATCH 162/427] make debug file optional --- activitypub.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 3e2d98f..1f42652 100644 --- a/activitypub.php +++ b/activitypub.php @@ -88,8 +88,9 @@ if ( \get_option( 'blog_public', 1 ) ) { Rest\NodeInfo::init(); } -if ( \WP_DEBUG ) { - require_once \dirname( __FILE__ ) . '/includes/debug.php'; +$debug_file = \dirname( __FILE__ ) . '/includes/debug.php'; +if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) { + require_once $debug_file; } /** From 068576342454e910d5fc4690545c37e89a0c4b50 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 11 May 2023 14:55:11 +0200 Subject: [PATCH 163/427] return error if class does not exist or is not readable --- activitypub.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 1f42652..c0a95fe 100644 --- a/activitypub.php +++ b/activitypub.php @@ -55,11 +55,11 @@ function init() { * Class Autoloader */ spl_autoload_register( - function ( $class ) { + function ( $full_class ) { $base_dir = \dirname( __FILE__ ) . '/includes/'; $base = 'activitypub'; - $class = strtolower( $class ); + $class = strtolower( $full_class ); if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) { $class = str_replace( 'activitypub\\', '', $class ); @@ -76,6 +76,9 @@ spl_autoload_register( if ( file_exists( $file ) && is_readable( $file ) ) { require_once $file; + } else { + // translators: %s is the class name + \wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) ); } } } From d16014911bffb0527a2597909d5005f713c5e7b5 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Thu, 11 May 2023 19:53:53 +0200 Subject: [PATCH 164/427] Compat: introduce a conditional to detect ActivityPub requests This conditional could be used within the plugin, but also by third-party plugins, to detect whether a request is an ActivityPub request, without having to manually check for query vars and headers every time. --- includes/functions.php | 44 ++++++++++++++++++++++++++++++++++++++++++ readme.txt | 1 + 2 files changed, 45 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index 19a3512..f1d6c65 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -228,3 +228,47 @@ function is_tombstone( $wp_error ) { return false; } + +/** + * Check if a request is for an ActivityPub request. + * + * @return bool False by default. + */ +function is_activitypub_request() { + global $wp_query; + + /* + * ActivityPub requests are currently only made for + * author archives, singular posts, and the homepage. + */ + if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + return false; + } + + // One can trigger an ActivityPub request by adding ?activitypub to the URL. + global $wp_query; + if ( isset( $wp_query->query_vars['activitypub'] ) ) { + return true; + } + + /* + * The other (more common) option to make an ActivityPub request + * is to send an Accept header. + */ + if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { + $accept = $_SERVER['HTTP_ACCEPT']; + + /* + * $accept can be a single value, or a comma separated list of values. + * We want to support both scenarios, + * and return true when the header includes at least one of the following: + * - application/activity+json + * - application/ld+json + */ + if ( preg_match( '/(application\/(ld\+json|activity\+json))/', $accept ) ) { + return true; + } + } + + return false; +} diff --git a/readme.txt b/readme.txt index 0004235..c08f455 100644 --- a/readme.txt +++ b/readme.txt @@ -115,6 +115,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu = Next = +* Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. From abfa7c796990bf6f09228c0164af2b490ecc3488 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Thu, 11 May 2023 13:25:30 -0500 Subject: [PATCH 165/427] Allow setting the REST namespace with `ACTIVITYPUB_REST_NAMESPACE` --- activitypub.php | 7 ++++--- includes/model/class-activity.php | 3 ++- includes/model/class-post.php | 3 ++- includes/rest/class-followers.php | 4 ++-- includes/rest/class-following.php | 4 ++-- includes/rest/class-inbox.php | 6 +++--- includes/rest/class-nodeinfo.php | 8 ++++---- includes/rest/class-ostatus.php | 2 +- includes/rest/class-outbox.php | 4 ++-- includes/rest/class-webfinger.php | 2 +- templates/author-json.php | 8 ++++---- templates/blog-json.php | 10 ++++++---- tests/test-class-activitypub-activity.php | 2 +- 13 files changed, 34 insertions(+), 29 deletions(-) diff --git a/activitypub.php b/activitypub.php index 2e731f2..6efc3c2 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); + defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); @@ -120,12 +121,12 @@ function plugin_settings_link( $actions ) { */ function add_rewrite_rules() { if ( ! \class_exists( 'Webfinger' ) ) { - \add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' ); + \add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger', 'top' ); } if ( ! \class_exists( 'Nodeinfo' ) || ! (bool) \get_option( 'blog_public', 1 ) ) { - \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); - \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); + \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery', 'top' ); + \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2', 'top' ); } \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index b5372dc..ce99e87 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -148,7 +148,8 @@ class Activity { $this->published = $object['published']; } - $this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) ); + $url = sprintf( '/%/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, intval( $post->get_post_author() ) ); + $this->add_to( \get_rest_url( null, $url ) ); if ( isset( $this->object['attributedTo'] ) ) { $this->actor = $this->object['attributedTo']; diff --git a/includes/model/class-post.php b/includes/model/class-post.php index ccb161c..022e390 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -142,7 +142,8 @@ class Post { */ public function __construct( $post ) { $this->post = \get_post( $post ); - $this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $this->get_post_author() ) . '/followers' ) ); + $url = sprintf( '/%/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, intval( $post->get_post_author() ) ); + $this->add_to( \get_rest_url( null, $url ) ); } /** diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 9a8b9b6..3abff6e 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -27,7 +27,7 @@ class Followers { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/users/(?P\d+)/followers', array( array( @@ -78,7 +78,7 @@ class Followers { $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore + $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 52a95e7..2606df4 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -21,7 +21,7 @@ class Following { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/users/(?P\d+)/following', array( array( @@ -72,7 +72,7 @@ class Following { $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/following" ); // phpcs:ignore + $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/following', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 5a23d02..16735df 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -26,7 +26,7 @@ class Inbox { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/inbox', array( array( @@ -39,7 +39,7 @@ class Inbox { ); \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/users/(?P\d+)/inbox', array( array( @@ -108,7 +108,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/inbox" ); // phpcs:ignore + $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/inbox', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 980c24b..bafa7e4 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -23,7 +23,7 @@ class Nodeinfo { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/nodeinfo/discovery', array( array( @@ -35,7 +35,7 @@ class Nodeinfo { ); \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/nodeinfo', array( array( @@ -47,7 +47,7 @@ class Nodeinfo { ); \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/nodeinfo2', array( array( @@ -173,7 +173,7 @@ class Nodeinfo { $discovery['links'] = array( array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => \get_rest_url( null, 'activitypub/1.0/nodeinfo' ), + 'href' => \get_rest_url( null, ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo' ), ), ); diff --git a/includes/rest/class-ostatus.php b/includes/rest/class-ostatus.php index 45ff901..415d502 100644 --- a/includes/rest/class-ostatus.php +++ b/includes/rest/class-ostatus.php @@ -14,7 +14,7 @@ class Ostatus { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/ostatus/remote-follow', array( array( diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 905dfd5..5ae8e50 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -21,7 +21,7 @@ class Outbox { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/users/(?P\d+)/outbox', array( array( @@ -72,7 +72,7 @@ class Outbox { $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/outbox" ); // phpcs:ignore + $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/outbox', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 10dcfa4..f75a3f7 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -25,7 +25,7 @@ class Webfinger { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/webfinger', array( array( diff --git a/templates/author-json.php b/templates/author-json.php index 3b355b9..090920d 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -28,10 +28,10 @@ if ( \has_header_image() ) { ); } -$json->inbox = \get_rest_url( null, "/activitypub/1.0/users/$author_id/inbox" ); -$json->outbox = \get_rest_url( null, "/activitypub/1.0/users/$author_id/outbox" ); -$json->followers = \get_rest_url( null, "/activitypub/1.0/users/$author_id/followers" ); -$json->following = \get_rest_url( null, "/activitypub/1.0/users/$author_id/following" ); +$json->inbox = \get_rest_url( null, sprintf( '/%s/users/%d/inbox', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); +$json->outbox = \get_rest_url( null, sprintf( '/%s/users/%d/outbox', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); +$json->followers = \get_rest_url( null, sprintf( '/%s/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); +$json->following = \get_rest_url( null, sprintf( '/%s/users/%d/following', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore diff --git a/templates/blog-json.php b/templates/blog-json.php index 4a1efc8..38f0406 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -27,10 +27,12 @@ if ( \has_header_image() ) { ); } -$json->inbox = \get_rest_url( null, '/activitypub/1.0/blog/inbox' ); -$json->outbox = \get_rest_url( null, '/activitypub/1.0/blog/outbox' ); -$json->followers = \get_rest_url( null, '/activitypub/1.0/blog/followers' ); -$json->following = \get_rest_url( null, '/activitypub/1.0/blog/following' ); +$blog_base = sprintf( '/%s/blog/', ACTIVITYPUB_REST_NAMESPACE ); + +$json->inbox = \get_rest_url( null, $blog_base . 'inbox' ); +$json->outbox = \get_rest_url( null, $blog_base . 'outbox' ); +$json->followers = \get_rest_url( null, $blog_base . 'followers' ); +$json->following = \get_rest_url( null, $blog_base . 'following' ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 7fe6551..b18c6ab 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_to() ); + $this->assertContains( \get_rest_url( null, '/' . ACTIVITYPUB_REST_NAMESPACE . '/users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); From 7d5cfb3078bfd2b4775b4edc6e22e5733db15946 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 12 May 2023 10:17:36 +0200 Subject: [PATCH 166/427] phpdoc --- includes/class-signature.php | 47 ++++++++++++++++++++++++---------- includes/rest/class-server.php | 11 ++++---- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 7c948f9..f82806a 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -9,13 +9,17 @@ use DateTimeZone; * ActivityPub Signature Class * * @author Matthias Pfefferle + * @author Django Doucet */ class Signature { /** - * @param int $user_id + * Return the public key for a given user. * - * @return mixed + * @param int $user_id The WordPress User ID. + * @param bool $force Force the generation of a new key pair. + * + * @return mixed The public key. */ public static function get_public_key( $user_id, $force = false ) { if ( $force ) { @@ -36,9 +40,12 @@ class Signature { } /** - * @param int $user_id + * Return the private key for a given user. * - * @return mixed + * @param int $user_id The WordPress User ID. + * @param bool $force Force the generation of a new key pair. + * + * @return mixed The private key. */ public static function get_private_key( $user_id, $force = false ) { if ( $force ) { @@ -61,7 +68,9 @@ class Signature { /** * Generates the pair keys * - * @param int $user_id + * @param int $user_id The WordPress User ID. + * + * @return void */ public static function generate_key_pair( $user_id ) { $config = array( @@ -92,6 +101,17 @@ class Signature { } } + /** + * Generates the Signature for a 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. + * + * @return string The signature. + */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $key = self::get_private_key( $user_id ); @@ -136,9 +156,9 @@ class Signature { /** * Verifies the http signatures * - * @param WP_REQUEST | Array $_SERVER - * @return void - * @author Django Doucet + * @param WP_REQUEST|array $request The request object or $_SERVER array. + * + * @return mixed A boolean or WP_Error. */ public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object @@ -217,8 +237,8 @@ class Signature { * Get public key from key_id * * @param string $key_id + * * @return string $publicKeyPem - * @author Django Doucet */ public static function get_remote_key( $key_id ) { // phpcs:ignore $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore @@ -235,8 +255,8 @@ class Signature { * Gets the signature algorithm from the signature header * * @param array $signature_block + * * @return string algorithm - * @author Django Doucet */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { @@ -254,8 +274,8 @@ class Signature { * Parses the Signature header * * @param array $header + * * @return array signature parts - * @author Django Doucet */ public static function parse_signature_header( $header ) { $ret = array(); @@ -293,9 +313,9 @@ class Signature { * * @param array $signed_headers * @param array $signature_block (pseudo-headers) - * @param array $headers (http headers) + * @param array $headers (http headers) + * * @return signed headers for comparison - * @author Django Doucet */ public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; @@ -360,6 +380,7 @@ class Signature { * for use with verify_http_signature() * * @param array $_SERVER + * * @return array $request */ public static function format_server_request( $server ) { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index a5219f3..7e8f720 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -41,7 +41,7 @@ class Server { /** * Render Application actor profile * - * @return WP_REST_Response + * @return WP_REST_Response The JSON profile of the Application Actor. */ public static function application_actor() { $json = new \stdClass(); @@ -72,11 +72,12 @@ class Server { * * @see \WP_REST_Request * - * @param $response - * @param $handler - * @param \WP_REST_Request $request + * @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 + * @return mixed|WP_Error The response, error, or modified response. */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); From 94e5539d75ef610f41c9a23195e0ba9c674513ee Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 12 May 2023 10:23:58 +0200 Subject: [PATCH 167/427] reset errors if new is set --- includes/model/class-follower.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 6c03fd8..a663270 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -224,6 +224,18 @@ class Follower { return null; } + /** + * Set new Error + * + * @param mixed $error The latest HTTP-Error. + * + * @return void + */ + public function set_error( $error ) { + $this->errors = array(); + $this->error = $error; + } + /** * Get the errors. * From 0b60944f93a947bbb458e82ad919338166ec087c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 May 2023 03:05:09 +0000 Subject: [PATCH 168/427] Update dealerdirect/phpcodesniffer-composer-installer requirement Updates the requirements on [dealerdirect/phpcodesniffer-composer-installer](https://github.com/PHPCSStandards/composer-installer) to permit the latest version. - [Release notes](https://github.com/PHPCSStandards/composer-installer/releases) - [Changelog](https://github.com/PHPCSStandards/composer-installer/blob/main/.github_changelog_generator) - [Commits](https://github.com/PHPCSStandards/composer-installer/compare/v0.7.1...v1.0.0) --- updated-dependencies: - dependency-name: dealerdirect/phpcodesniffer-composer-installer dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 67bd2d3..b0117c4 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "*", "yoast/phpunit-polyfills": "^1.0", - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.1" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0" }, "config": { "allow-plugins": true From 314ccf43a6915d87379862383a369be95c2e4bdd Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 12 May 2023 14:58:50 -0500 Subject: [PATCH 169/427] add a `get_rest_url_by_path` helper function, and use it --- includes/functions.php | 13 +++++++++++++ includes/model/class-activity.php | 4 ++-- includes/model/class-post.php | 4 ++-- includes/rest/class-followers.php | 2 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 2 +- includes/rest/class-nodeinfo.php | 2 +- includes/rest/class-outbox.php | 2 +- includes/rest/class-webfinger.php | 1 + templates/author-json.php | 8 ++++---- templates/blog-json.php | 10 ++++------ tests/test-class-activitypub-activity.php | 2 +- 12 files changed, 32 insertions(+), 20 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 19a3512..41f0d46 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -228,3 +228,16 @@ function is_tombstone( $wp_error ) { return false; } + +/** + * Get the REST URL relative to this plugin's namespace. + * + * @param string $path Optional. REST route path. Otherwise this plugin's namespaced root. + * @return string REST URL relative to this plugin's namespace. + */ +function get_rest_url_by_path( $path = '' ) { + // we'll handle the leading slash. + $path = ltrim( $path, '/' ); + $url = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path ); + return \get_rest_url( null, $url ); +} \ No newline at end of file diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index ce99e87..32dd0f5 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -148,8 +148,8 @@ class Activity { $this->published = $object['published']; } - $url = sprintf( '/%/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, intval( $post->get_post_author() ) ); - $this->add_to( \get_rest_url( null, $url ) ); + $path = sprintf( 'users/%d/followers', intval( $post->get_post_author() ) ); + $this->add_to( get_rest_url_by_path( $path ) ); if ( isset( $this->object['attributedTo'] ) ) { $this->actor = $this->object['attributedTo']; diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 022e390..528182c 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -142,8 +142,8 @@ class Post { */ public function __construct( $post ) { $this->post = \get_post( $post ); - $url = sprintf( '/%/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, intval( $post->get_post_author() ) ); - $this->add_to( \get_rest_url( null, $url ) ); + $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) ); + $this->add_to( get_rest_url_by_path( $path ) ); } /** diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 3abff6e..1ec2c78 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -78,7 +78,7 @@ class Followers { $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore $json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 2606df4..0c685d5 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -72,7 +72,7 @@ class Following { $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/following', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 16735df..a73a783 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -108,7 +108,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/inbox', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index bafa7e4..3b3c6dc 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -173,7 +173,7 @@ class Nodeinfo { $discovery['links'] = array( array( 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => \get_rest_url( null, ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo' ), + 'href' => get_rest_url_by_path( 'nodeinfo' ), ), ); diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 5ae8e50..1a52750 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -72,7 +72,7 @@ class Outbox { $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = \get_author_posts_url( $user_id ); $json->type = 'OrderedCollectionPage'; - $json->partOf = \get_rest_url( null, sprintf( '/%s/users/%d/outbox', ACTIVITYPUB_REST_NAMESPACE, $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f75a3f7..b04839b 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -24,6 +24,7 @@ class Webfinger { * Register routes */ public static function register_routes() { + \l( 'register webfinger' ); \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, '/webfinger', diff --git a/templates/author-json.php b/templates/author-json.php index 090920d..cf4b85e 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -28,10 +28,10 @@ if ( \has_header_image() ) { ); } -$json->inbox = \get_rest_url( null, sprintf( '/%s/users/%d/inbox', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); -$json->outbox = \get_rest_url( null, sprintf( '/%s/users/%d/outbox', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); -$json->followers = \get_rest_url( null, sprintf( '/%s/users/%d/followers', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); -$json->following = \get_rest_url( null, sprintf( '/%s/users/%d/following', ACTIVITYPUB_REST_NAMESPACE, $author_id ) ); +$json->inbox = get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) ); +$json->outbox = get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) ); +$json->followers = get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) ); +$json->following = get_rest_url_by_path( sprintf( 'users/%d/following', $author_id ) ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore diff --git a/templates/blog-json.php b/templates/blog-json.php index 38f0406..a988385 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -27,12 +27,10 @@ if ( \has_header_image() ) { ); } -$blog_base = sprintf( '/%s/blog/', ACTIVITYPUB_REST_NAMESPACE ); - -$json->inbox = \get_rest_url( null, $blog_base . 'inbox' ); -$json->outbox = \get_rest_url( null, $blog_base . 'outbox' ); -$json->followers = \get_rest_url( null, $blog_base . 'followers' ); -$json->following = \get_rest_url( null, $blog_base . 'following' ); +$json->inbox = get_rest_url_by_path( 'blog/inbox' ); +$json->outbox = get_rest_url_by_path( 'blog/outbox' ); +$json->followers = get_rest_url_by_path( 'blog/followers' ); +$json->following = get_rest_url_by_path( 'blog/following' ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index b18c6ab..6569652 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( \get_rest_url( null, '/' . ACTIVITYPUB_REST_NAMESPACE . '/users/1/followers' ), $activitypub_activity->get_to() ); + $this->assertContains( get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); From 3fa5e4f37ec85ad3ab2eb912caed2d76d24e3186 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 12 May 2023 15:31:53 -0500 Subject: [PATCH 170/427] now with more `use` --- includes/model/class-activity.php | 2 ++ includes/model/class-post.php | 2 ++ includes/rest/class-followers.php | 1 + includes/rest/class-following.php | 2 ++ includes/rest/class-inbox.php | 1 + includes/rest/class-nodeinfo.php | 2 ++ includes/rest/class-outbox.php | 2 ++ templates/author-json.php | 2 ++ templates/blog-json.php | 2 ++ tests/test-class-activitypub-activity.php | 2 +- 10 files changed, 17 insertions(+), 1 deletion(-) diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index 32dd0f5..4bd0d5e 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -1,6 +1,8 @@ {'@context'} = \Activitypub\get_context(); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 6569652..af0ed55 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); + $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); From 5a91fdcf0ad73a1555d5928ac57a1db0399df123 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 12 May 2023 15:43:04 -0500 Subject: [PATCH 171/427] remove debug log --- includes/rest/class-webfinger.php | 1 - 1 file changed, 1 deletion(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index b04839b..f75a3f7 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -24,7 +24,6 @@ class Webfinger { * Register routes */ public static function register_routes() { - \l( 'register webfinger' ); \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, '/webfinger', From ec00ace234816cc7d4b4cbad6a7674d69834f508 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 12 May 2023 16:42:30 -0500 Subject: [PATCH 172/427] add a `activitypub_rest_url` filter --- includes/functions.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 39ba262..7d6f86c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -238,8 +238,11 @@ function is_tombstone( $wp_error ) { function get_rest_url_by_path( $path = '' ) { // we'll handle the leading slash. $path = ltrim( $path, '/' ); - $url = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path ); - return \get_rest_url( null, $url ); + $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path ); + $rest_url = \get_rest_url( null, $namespaced_path ); + // Just in case there are non-default ways of handling REST URLs. + $rest_url = \apply_filters( 'activitypub_rest_url', $rest_url, $path, ACTIVITYPUB_REST_NAMESPACE ); + return $rest_url; } /** From 31e7e44642258cdc7bd5311c426af478d2eea0bd Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 12 May 2023 18:25:49 -0500 Subject: [PATCH 173/427] remove filter --- includes/functions.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 7d6f86c..8ef0c35 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -239,10 +239,7 @@ function get_rest_url_by_path( $path = '' ) { // we'll handle the leading slash. $path = ltrim( $path, '/' ); $namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path ); - $rest_url = \get_rest_url( null, $namespaced_path ); - // Just in case there are non-default ways of handling REST URLs. - $rest_url = \apply_filters( 'activitypub_rest_url', $rest_url, $path, ACTIVITYPUB_REST_NAMESPACE ); - return $rest_url; + return \get_rest_url( null, $namespaced_path ); } /** From 7456d36834acf459f0fcdc489e457f9ff06491f5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 15 May 2023 10:48:34 +0200 Subject: [PATCH 174/427] use const instead of -1 --- activitypub.php | 1 + includes/class-http.php | 3 ++- includes/class-signature.php | 9 +++++---- includes/model/class-user.php | 23 +++++++++++++++++++++++ includes/rest/class-server.php | 3 ++- 5 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 includes/model/class-user.php diff --git a/activitypub.php b/activitypub.php index 049d7d7..4292684 100644 --- a/activitypub.php +++ b/activitypub.php @@ -41,6 +41,7 @@ function init() { require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-post.php'; + require_once \dirname( __FILE__ ) . '/includes/model/class-user.php'; require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php'; require_once \dirname( __FILE__ ) . '/includes/class-migration.php'; diff --git a/includes/class-http.php b/includes/class-http.php index 247d87e..58cace9 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,6 +2,7 @@ namespace Activitypub; use WP_Error; +use Activitypub\Model\User; /** * ActivityPub HTTP Class @@ -62,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( -1, 'get', $url, $date ); + $signature = Signature::generate_signature( User::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index f82806a..9472128 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -4,6 +4,7 @@ namespace Activitypub; use WP_Error; use DateTime; use DateTimeZone; +use Activitypub\Model\User; /** * ActivityPub Signature Class @@ -26,7 +27,7 @@ class Signature { self::generate_key_pair( $user_id ); } - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key = \get_option( 'activitypub_magic_sig_public_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_public_key', true ); @@ -52,7 +53,7 @@ class Signature { self::generate_key_pair( $user_id ); } - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key = \get_option( 'activitypub_magic_sig_private_key' ); } else { $key = \get_user_meta( $user_id, 'magic_sig_private_key', true ); @@ -85,7 +86,7 @@ class Signature { \openssl_pkey_export( $key, $priv_key ); $detail = \openssl_pkey_get_details( $key ); - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { // private key \update_option( 'activitypub_magic_sig_private_key', $priv_key ); @@ -140,7 +141,7 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - if ( -1 === $user_id ) { + if ( User::APPLICATION_USER_ID === $user_id ) { $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); } else { $key_id = \get_author_posts_url( $user_id ) . '#main-key'; diff --git a/includes/model/class-user.php b/includes/model/class-user.php new file mode 100644 index 0000000..5c62473 --- /dev/null +++ b/includes/model/class-user.php @@ -0,0 +1,23 @@ +publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), - 'publicKeyPem' => Signature::get_public_key( -1 ), // phpcs:ignore WordPress.NamingConventions + 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions ); $response = new WP_REST_Response( $json, 200 ); From 8b9026ab5ec3b427cfb9c23925c4506aba6d6b09 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 15 May 2023 10:55:07 +0200 Subject: [PATCH 175/427] fix get_post_content_template function --- includes/model/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index ccb161c..e54c295 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -537,6 +537,6 @@ class Post { return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]"; } - return $content; + return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); } } From 12724a3681b12ad8e369737fd458e44ade51f3be Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Sun, 14 May 2023 22:53:11 -0600 Subject: [PATCH 176/427] Switch secure_mode to a filter --- activitypub.php | 1 + includes/class-activitypub.php | 2 +- includes/class-admin.php | 18 +++++++++--------- includes/rest/class-server.php | 2 +- templates/settings.php | 6 +++--- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/activitypub.php b/activitypub.php index 4292684..378e999 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,6 +25,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

[ap_title]

\n\n[ap_content]\n\n

[ap_hashtags]

\n\n

[ap_shortlink]

" ); + \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 30cc7d0..f7885cd 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -78,7 +78,7 @@ class Activitypub { \in_array( 'application/ld+json', $accept, true ) || \in_array( 'application/json', $accept, true ) ) { - if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { + if ( ACTIVITYPUB_SECURE_MODE ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { // fallback as template_loader can't return http headers diff --git a/includes/class-admin.php b/includes/class-admin.php index 7a19e8e..4bc7b9c 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,15 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - \register_setting( - 'activitypub', - 'activitypub_use_secure_mode', - array( - 'type' => 'boolean', - 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - 'default' => 0, - ) - ); + // \register_setting( + // 'activitypub', + // 'activitypub_use_secure_mode', + // array( + // 'type' => 'boolean', + // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), + // 'default' => 0, + // ) + // ); } public static function schedule_migration() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index a2d837a..b5b55fd 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -94,7 +94,7 @@ class Server { } else { if ( '/activitypub/1.0/webfinger' !== $route ) { // SecureMode/Authorized fetch. - if ( \get_option( 'activitypub_use_secure_mode', '0' ) ) { + if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; diff --git a/templates/settings.php b/templates/settings.php index 6429688..1966c4f 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,7 +171,7 @@ - + From e79f2e89913c949f6a4d1c0eeb783c6fa0b937a6 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Tue, 16 May 2023 00:11:27 -0600 Subject: [PATCH 177/427] fix keyId url --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 9472128..0ef8bcb 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -142,7 +142,7 @@ class Signature { $signature = \base64_encode( $signature ); // phpcs:ignore if ( User::APPLICATION_USER_ID === $user_id ) { - $key_id = \get_rest_url( null, 'activitypub/1.0/service#main-key' ); + $key_id = \get_rest_url( null, 'activitypub/1.0/application#main-key' ); } else { $key_id = \get_author_posts_url( $user_id ) . '#main-key'; } From 9cd2a049557b9abd567df8a5b64f0acb767da9d4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 16 May 2023 08:14:04 +0200 Subject: [PATCH 178/427] re-added some namespace consts --- activitypub.php | 2 +- includes/class-activitypub.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/activitypub.php b/activitypub.php index 817f553..71e7261 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,7 +25,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); - defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); + \defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 37bedf4..c5d0e71 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -225,7 +225,7 @@ class Activitypub { if ( ! \class_exists( 'Webfinger' ) ) { \add_rewrite_rule( '^.well-known/webfinger', - 'index.php?rest_route=/activitypub/1.0/webfinger', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger', 'top' ); } @@ -233,12 +233,12 @@ class Activitypub { if ( ! \class_exists( 'Nodeinfo' ) && true === (bool) \get_option( 'blog_public', 1 ) ) { \add_rewrite_rule( '^.well-known/nodeinfo', - 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery', 'top' ); \add_rewrite_rule( '^.well-known/x-nodeinfo2', - 'index.php?rest_route=/activitypub/1.0/nodeinfo2', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2', 'top' ); } From 49ee03f1f1637e50079152ca550cafaf58daa4d5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 16 May 2023 10:01:23 +0200 Subject: [PATCH 179/427] fix indents --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 84460df..691a318 100644 --- a/activitypub.php +++ b/activitypub.php @@ -25,7 +25,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); - \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); + \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); From 3d16b8de1d46f13c6ec2d14cd5e5ee6e678e3d85 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 09:01:28 +0200 Subject: [PATCH 180/427] use full function name in templates --- templates/author-json.php | 10 ++++------ templates/blog-json.php | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/templates/author-json.php b/templates/author-json.php index f132b4b..7b112ac 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,6 +1,4 @@ inbox = get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) ); -$json->outbox = get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) ); -$json->followers = get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) ); -$json->following = get_rest_url_by_path( sprintf( 'users/%d/following', $author_id ) ); +$json->inbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) ); +$json->outbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) ); +$json->followers = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) ); +$json->following = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/following', $author_id ) ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore diff --git a/templates/blog-json.php b/templates/blog-json.php index 2da9c65..b87bc94 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,6 +1,4 @@ {'@context'} = \Activitypub\get_context(); @@ -29,10 +27,10 @@ if ( \has_header_image() ) { ); } -$json->inbox = get_rest_url_by_path( 'blog/inbox' ); -$json->outbox = get_rest_url_by_path( 'blog/outbox' ); -$json->followers = get_rest_url_by_path( 'blog/followers' ); -$json->following = get_rest_url_by_path( 'blog/following' ); +$json->inbox = \Activitypub\get_rest_url_by_path( 'blog/inbox' ); +$json->outbox = \Activitypub\get_rest_url_by_path( 'blog/outbox' ); +$json->followers = \Activitypub\get_rest_url_by_path( 'blog/followers' ); +$json->following = \Activitypub\get_rest_url_by_path( 'blog/following' ); $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore From d89c05aa49801ad5974179345440c31bc55fd8e0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 09:02:09 +0200 Subject: [PATCH 181/427] init missing Nodeinfo endpoint --- activitypub.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 71e7261..3a77944 100644 --- a/activitypub.php +++ b/activitypub.php @@ -32,8 +32,8 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); Migration::init(); - Activity_Dispatcher::init(); Activitypub::init(); + Activity_Dispatcher::init(); Collection\Followers::init(); // Configure the REST API route @@ -41,13 +41,13 @@ function init() { Rest\Inbox::init(); Rest\Followers::init(); Rest\Following::init(); + Rest\Nodeinfo::init(); Rest\Webfinger::init(); Admin::init(); Hashtag::init(); Shortcodes::init(); Mention::init(); - Debug::init(); Health_Check::init(); Scheduler::init(); } @@ -96,6 +96,7 @@ if ( \get_option( 'blog_public', 1 ) ) { $debug_file = \dirname( __FILE__ ) . '/includes/debug.php'; if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) { require_once $debug_file; + Debug::init(); } /** From 60fc581e1de7e6bfba9ab7dc7610102f394359a0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 09:02:37 +0200 Subject: [PATCH 182/427] coding style --- includes/rest/class-followers.php | 1 + includes/rest/class-inbox.php | 1 + 2 files changed, 2 insertions(+) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index b2983ae..fc05bdd 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -6,6 +6,7 @@ use stdClass; use WP_REST_Server; use WP_REST_Response; use Activitypub\Collection\Followers as FollowerCollection; + use function Activitypub\get_rest_url_by_path; /** diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 7e2da60..b4765d3 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -2,6 +2,7 @@ namespace Activitypub\Rest; use Activitypub\Model\Activity; + use function Activitypub\get_rest_url_by_path; /** From c34fb74b41b5bc84b38a33b0b9783fb8dd7c7d42 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 09:03:26 +0200 Subject: [PATCH 183/427] coding style --- includes/functions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/functions.php b/includes/functions.php index 8ef0c35..5bcf97a 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -233,6 +233,7 @@ function is_tombstone( $wp_error ) { * Get the REST URL relative to this plugin's namespace. * * @param string $path Optional. REST route path. Otherwise this plugin's namespaced root. + * * @return string REST URL relative to this plugin's namespace. */ function get_rest_url_by_path( $path = '' ) { From a147d21fdabbb1655b6833567e2a34cc5784f020 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 10:25:00 +0200 Subject: [PATCH 184/427] Update activitypub.php NodeInfo is only initialized when blog is public --- activitypub.php | 1 - 1 file changed, 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 3a77944..7448e46 100644 --- a/activitypub.php +++ b/activitypub.php @@ -41,7 +41,6 @@ function init() { Rest\Inbox::init(); Rest\Followers::init(); Rest\Following::init(); - Rest\Nodeinfo::init(); Rest\Webfinger::init(); Admin::init(); From ec3f8454c13a75b6e0f5f38e13e317c0b594e98a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 17 May 2023 10:25:31 +0200 Subject: [PATCH 185/427] Update activitypub.php --- activitypub.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/activitypub.php b/activitypub.php index 7448e46..5b19dce 100644 --- a/activitypub.php +++ b/activitypub.php @@ -128,7 +128,7 @@ function plugin_settings_link( $actions ) { ) ); -register_uninstall_hook( +\register_uninstall_hook( __FILE__, array( __NAMESPACE__ . '\Activitypub', From 5e4c68ab66ea0bfd3fbb9a518c6ba67aaae7954a Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 17 May 2023 23:49:33 -0600 Subject: [PATCH 186/427] server init --- activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/activitypub.php b/activitypub.php index 2f74c2f..b3f6d7f 100644 --- a/activitypub.php +++ b/activitypub.php @@ -44,6 +44,7 @@ function init() { Rest\Following::init(); Rest\Nodeinfo::init(); Rest\Webfinger::init(); + Rest\Server::init(); Admin::init(); Hashtag::init(); From ed77ffce264515f74de56abd3b4c59f3560c9624 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Thu, 18 May 2023 00:03:11 -0600 Subject: [PATCH 187/427] update rest paths to namespace --- includes/rest/class-server.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index b5b55fd..c99bb34 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -4,6 +4,8 @@ namespace Activitypub\Rest; use WP_REST_Response; use Activitypub\Signature; use Activitypub\Model\User; +use function Activitypub\get_rest_url_by_path; + /** * ActivityPub Server REST-Class @@ -18,8 +20,8 @@ class Server { * Initialize the class, registering WordPress hooks */ public static function init() { - \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 ); \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); + \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 ); } /** @@ -27,7 +29,7 @@ class Server { */ public static function register_routes() { \register_rest_route( - 'activitypub/1.0', + ACTIVITYPUB_REST_NAMESPACE, '/application', array( array( @@ -48,16 +50,16 @@ class Server { $json = new \stdClass(); $json->{'@context'} = \Activitypub\get_context(); - $json->id = \get_rest_url( null, 'activitypub/1.0/application' ); + $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; - $json->preferredUsername = wp_parse_url( get_site_url(), PHP_URL_HOST ); // phpcs:ignore WordPress.NamingConventions + $json->preferredUsername = str_replace( array( '.' ), '-' , wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions - 'id' => \get_rest_url( null, 'activitypub/1.0/application#main-key' ), - 'owner' => \get_rest_url( null, 'activitypub/1.0/application' ), + 'id' => get_rest_url_by_path( 'application#main-key' ), + 'owner' => get_rest_url_by_path( 'application' ), 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions ); @@ -92,7 +94,7 @@ class Server { return $verified_request; } } else { - if ( '/activitypub/1.0/webfinger' !== $route ) { + if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { // SecureMode/Authorized fetch. if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); From f4aadc00fcc41b2c21d8b69b6902e5b5a899ece1 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Thu, 18 May 2023 00:10:03 -0600 Subject: [PATCH 188/427] phpcs --- includes/rest/class-server.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index c99bb34..dfee380 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -52,7 +52,7 @@ class Server { $json->{'@context'} = \Activitypub\get_context(); $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; - $json->preferredUsername = str_replace( array( '.' ), '-' , wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions + $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); $json->summary = 'WordPress-ActivityPub application actor'; $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions From 6a0fc43a05e56308965854126be745d17aaf109e Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Thu, 18 May 2023 19:30:08 -0500 Subject: [PATCH 189/427] Set `ACTIVITYPUB_REST_NAMESPACE` outside of `init` Needed to prevent activation errors. --- activitypub.php | 4 ++-- includes/rest/class-outbox.php | 3 +++ includes/rest/class-webfinger.php | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 5b19dce..3f582ea 100644 --- a/activitypub.php +++ b/activitypub.php @@ -15,6 +15,8 @@ namespace Activitypub; +\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); + /** * Initialize plugin */ @@ -25,8 +27,6 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); - \defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); - \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index abffbe9..087c0ef 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -80,6 +80,8 @@ class Outbox { // phpcs:ignore $json->totalItems = 0; + // We can query this more directly based on the supplied post types. + // And cache these counts and invalidate them on publish_post. foreach ( $post_types as $post_type ) { $count_posts = \wp_count_posts( $post_type ); $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore @@ -142,6 +144,7 @@ class Outbox { 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param, $request, $key ) { + // this is probably ok on multisite still? return user_can( $param, 'publish_posts' ); }, ); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f75a3f7..e7c00fa 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -62,6 +62,7 @@ class Webfinger { } $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); + /// YIKES NOPE NOPE NOT ON DOTCOM! if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); From d7d6ebbc1fd231ee4d470a2e23d3ad49eaa681e6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 11:43:54 +0200 Subject: [PATCH 190/427] remove comments @mattwiebe maybe you can add them as issues --- includes/rest/class-outbox.php | 1 - includes/rest/class-webfinger.php | 1 - 2 files changed, 2 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 087c0ef..9fff8c7 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -144,7 +144,6 @@ class Outbox { 'required' => true, 'type' => 'integer', 'validate_callback' => function( $param, $request, $key ) { - // this is probably ok on multisite still? return user_can( $param, 'publish_posts' ); }, ); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index e7c00fa..f75a3f7 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -62,7 +62,6 @@ class Webfinger { } $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); - /// YIKES NOPE NOPE NOT ON DOTCOM! if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); From 70c3b3fd517c3dbb7064e746e792b004d920182e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 11:45:12 +0200 Subject: [PATCH 191/427] remove comments --- includes/rest/class-outbox.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 9fff8c7..abffbe9 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -80,8 +80,6 @@ class Outbox { // phpcs:ignore $json->totalItems = 0; - // We can query this more directly based on the supplied post types. - // And cache these counts and invalidate them on publish_post. foreach ( $post_types as $post_type ) { $count_posts = \wp_count_posts( $post_type ); $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore From dd486e552ff40e2bdbd9153ba04afd9d93a1782d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 12:00:11 +0200 Subject: [PATCH 192/427] some code cleanups --- includes/rest/class-inbox.php | 28 +++++++++++++++++----------- includes/rest/class-outbox.php | 21 ++++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b4765d3..2345769 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -1,9 +1,15 @@ \WP_REST_Server::EDITABLE, + 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( self::class, 'shared_inbox_post' ), 'args' => self::shared_inbox_post_parameters(), 'permission_callback' => '__return_true', @@ -45,13 +51,13 @@ class Inbox { '/users/(?P\d+)/inbox', array( array( - 'methods' => \WP_REST_Server::EDITABLE, + 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( self::class, 'user_inbox_post' ), 'args' => self::user_inbox_post_parameters(), 'permission_callback' => '__return_true', ), array( - 'methods' => \WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::READABLE, 'callback' => array( self::class, 'user_inbox_get' ), 'args' => self::user_inbox_get_parameters(), 'permission_callback' => '__return_true', @@ -106,7 +112,7 @@ class Inbox { $json = new \stdClass(); - $json->{'@context'} = \Activitypub\get_context(); + $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; @@ -126,7 +132,7 @@ class Inbox { */ \do_action( 'activitypub_inbox_post' ); - $response = new \WP_REST_Response( $json, 200 ); + $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); @@ -150,7 +156,7 @@ class Inbox { \do_action( 'activitypub_inbox', $data, $user_id, $type ); \do_action( "activitypub_inbox_{$type}", $data, $user_id ); - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -166,7 +172,7 @@ class Inbox { $users = self::extract_recipients( $data ); if ( ! $users ) { - return new \WP_Error( + return new WP_Error( 'rest_invalid_param', \__( 'No recipients found', 'activitypub' ), array( @@ -189,7 +195,7 @@ class Inbox { \do_action( "activitypub_inbox_{$type}", $data, $user->ID ); } - return new \WP_REST_Response( array(), 202 ); + return new WP_REST_Response( array(), 202 ); } /** @@ -350,7 +356,7 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_reaction( $object, $user_id ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + $meta = get_remote_metadata_by_actor( $object['actor'] ); $comment_post_id = \url_to_postid( $object['object'] ); @@ -395,7 +401,7 @@ class Inbox { * @param int $user_id The id of the local blog-user */ public static function handle_create( $object, $user_id ) { - $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); + $meta = get_remote_metadata_by_actor( $object['actor'] ); if ( ! isset( $object['object']['inReplyTo'] ) ) { return; @@ -502,7 +508,7 @@ class Inbox { $users = array(); foreach ( $recipients as $recipient ) { - $user_id = \Activitypub\url_to_authorid( $recipient ); + $user_id = url_to_authorid( $recipient ); $user = get_user_by( 'id', $user_id ); diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index abffbe9..d6b0757 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -1,6 +1,13 @@ \d+)/outbox', array( array( - 'methods' => \WP_REST_Server::READABLE, + 'methods' => WP_REST_Server::READABLE, 'callback' => array( self::class, 'user_outbox_get' ), 'args' => self::request_parameters(), 'permission_callback' => '__return_true', @@ -48,7 +55,7 @@ class Outbox { $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); if ( ! $author ) { - return new \WP_Error( + return new WP_Error( 'rest_invalid_param', \__( 'User not found', 'activitypub' ), array( @@ -67,9 +74,9 @@ class Outbox { */ \do_action( 'activitypub_outbox_pre' ); - $json = new \stdClass(); + $json = new stdClass(); - $json->{'@context'} = \Activitypub\get_context(); + $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = \get_author_posts_url( $user_id ); @@ -103,8 +110,8 @@ class Outbox { ); foreach ( $posts as $post ) { - $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create', false ); + $activitypub_post = new Post( $post ); + $activitypub_activity = new Activity( 'Create', false ); $activitypub_activity->from_post( $activitypub_post ); $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore @@ -119,7 +126,7 @@ class Outbox { */ \do_action( 'activitypub_outbox_post' ); - $response = new \WP_REST_Response( $json, 200 ); + $response = new WP_REST_Response( $json, 200 ); $response->header( 'Content-Type', 'application/activity+json' ); From e48ce0ebceedc1a84d02fe0bcc3ce031a1455ab9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 17:16:19 +0200 Subject: [PATCH 193/427] I would remove the settings for now --- includes/class-admin.php | 9 --------- templates/settings.php | 15 --------------- 2 files changed, 24 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 4bc7b9c..fe44ed4 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -144,15 +144,6 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); - // \register_setting( - // 'activitypub', - // 'activitypub_use_secure_mode', - // array( - // 'type' => 'boolean', - // 'description' => \__( 'Secure mode allows blocking servers from fetching public activities', 'activitypub' ), - // 'default' => 0, - // ) - // ); } public static function schedule_migration() { diff --git a/templates/settings.php b/templates/settings.php index 1966c4f..2bfaed4 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -171,21 +171,6 @@

- - From a1753242f3ea19b085a19c83fd0a09753720016d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 18:03:05 +0200 Subject: [PATCH 194/427] fix missing namespace --- includes/rest/class-outbox.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index d6b0757..2138f71 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -8,6 +8,7 @@ use WP_REST_Response; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; /** From e04ccdc961b62a5ad15cfda759f44e2bb74f0da9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 18:06:39 +0200 Subject: [PATCH 195/427] fix missing namespace --- includes/rest/class-outbox.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index d6b0757..2138f71 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -8,6 +8,7 @@ use WP_REST_Response; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; /** From 25b53887efaeaa24e26b844f8218a02f0b6fb4bf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 19 May 2023 22:37:05 +0200 Subject: [PATCH 196/427] code improvements --- activitypub.php | 4 ++-- integration/class-buddypress.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/activitypub.php b/activitypub.php index 3f582ea..a1cd6df 100644 --- a/activitypub.php +++ b/activitypub.php @@ -50,7 +50,7 @@ function init() { Health_Check::init(); Scheduler::init(); } -\add_action( 'plugins_loaded', '\Activitypub\init' ); +\add_action( 'plugins_loaded', __NAMESPACE__ . '\init' ); /** * Class Autoloader @@ -143,7 +143,7 @@ function enable_buddypress_features() { require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; Integration\Buddypress::init(); } -add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); +add_action( 'bp_include', __NAMESPACE__ . '\enable_buddypress_features' ); /** * `get_plugin_data` wrapper diff --git a/integration/class-buddypress.php b/integration/class-buddypress.php index 1087f47..e4ec40e 100644 --- a/integration/class-buddypress.php +++ b/integration/class-buddypress.php @@ -3,7 +3,7 @@ namespace Activitypub\Integration; class Buddypress { public static function init() { - \add_filter( 'activitypub_json_author_array', array( 'Activitypub\Integration\Buddypress', 'add_user_metadata' ), 11, 2 ); + \add_filter( 'activitypub_json_author_array', array( self::class, 'add_user_metadata' ), 11, 2 ); } public static function add_user_metadata( $object, $author_id ) { From 68002db291e8952fd3b79c24363333f87d564a49 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 10:58:13 +0200 Subject: [PATCH 197/427] prevent sweeping of followers taxonomies thanks @akirk https://github.com/polylang/polylang/commit/b0db9db87ea4c765f85c4478454bc4b95ebf8917 --- activitypub.php | 25 ++++++++++--- includes/collection/class-followers.php | 19 ++++++++++ integration/class-wp-sweep.php | 50 +++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 integration/class-wp-sweep.php diff --git a/activitypub.php b/activitypub.php index a1cd6df..e19751f 100644 --- a/activitypub.php +++ b/activitypub.php @@ -27,6 +27,7 @@ function init() { \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_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); + \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); @@ -139,11 +140,25 @@ function plugin_settings_link( $actions ) { /** * Only load code that needs BuddyPress to run once BP is loaded and initialized. */ -function enable_buddypress_features() { - require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; - Integration\Buddypress::init(); -} -add_action( 'bp_include', __NAMESPACE__ . '\enable_buddypress_features' ); +add_action( + 'bp_include', + function() { + require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; + Integration\Buddypress::init(); + }, + 0 +); + +add_action( + 'plugins_loaded', + function() { + if ( defined( 'WP_SWEEP_VERSION' ) ) { + require_once \dirname( __FILE__ ) . '/integration/class-wp-sweep.php'; + Integration\Wp_Sweep::init(); + } + }, + 0 +); /** * `get_plugin_data` wrapper diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 8a271f6..c7e55c2 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -338,6 +338,25 @@ class Followers { return $items; } + /** + * Get all Followers + * + * @param array $args The WP_Term_Query arguments. + * + * @return array The Term list of Followers. + */ + public static function get_all_followers( $args = array() ) { + $defaults = array( + 'taxonomy' => self::TAXONOMY, + 'hide_empty' => false, + ); + + $args = wp_parse_args( $args, $defaults ); + $terms = new WP_Term_Query( $args ); + + return $terms->get_terms(); + } + /** * Count the total number of followers * diff --git a/integration/class-wp-sweep.php b/integration/class-wp-sweep.php new file mode 100644 index 0000000..730edba --- /dev/null +++ b/integration/class-wp-sweep.php @@ -0,0 +1,50 @@ + 'ids' ) ); + + $excluded_term_ids = array_merge( $excluded_term_ids, $followers ); + + return array_unique( $excluded_term_ids ); + } +} From 467a349b1618240e9b9efb6aa0a05ddeed351ff9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 11:31:46 +0200 Subject: [PATCH 198/427] some small improvements --- includes/rest/class-server.php | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index dfee380..8962db8 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -1,9 +1,12 @@ {'@context'} = \Activitypub\get_context(); + $json->{'@context'} = get_context(); $json->id = get_rest_url_by_path( 'application' ); $json->type = 'Application'; $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions $json->name = get_bloginfo( 'name' ); - $json->summary = 'WordPress-ActivityPub application actor'; + $json->summary = __( 'WordPress-ActivityPub application actor', 'activitypub' ); $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax - $json->publicKey = (object) array( // phpcs:ignore WordPress.NamingConventions + $json->publicKey = array( // phpcs:ignore WordPress.NamingConventions 'id' => get_rest_url_by_path( 'application#main-key' ), 'owner' => get_rest_url_by_path( 'application' ), 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions @@ -73,7 +76,7 @@ class Server { /** * Callback function to authorize each api requests * - * @see \WP_REST_Request + * @see WP_REST_Request * * @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. @@ -84,23 +87,25 @@ class Server { */ public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); + if ( ! str_starts_with( $route, '/activitypub' ) ) { return $response; } + if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { + return $response; + } + if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; } - } else { - if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { - // SecureMode/Authorized fetch. - if ( ACTIVITYPUB_SECURE_MODE ) { - $verified_request = Signature::verify_http_signature( $request ); - if ( \is_wp_error( $verified_request ) ) { - return $verified_request; - } + } elseif ( 'GET' === $request->get_method() ) { + if ( ACTIVITYPUB_SECURE_MODE ) { + $verified_request = Signature::verify_http_signature( $request ); + if ( \is_wp_error( $verified_request ) ) { + return $verified_request; } } } From c1b644aee1f9059fba4714d402284755a94150b3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 11:33:02 +0200 Subject: [PATCH 199/427] Fix #339 --- README.md | 4 +++- readme.txt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7a9662..e434e69 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U Add the following to the .htaccess file in the root directory: - RedirectMatch "^\/\.well-known(.*)$" "\/blog\/\.well-known$1" + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1" Where 'blog' is the path to the subdirectory at which your blog resides. @@ -115,6 +115,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu ### Next ### +* Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. +* Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. ### 0.17.0 ### diff --git a/readme.txt b/readme.txt index c08f455..85a02e8 100644 --- a/readme.txt +++ b/readme.txt @@ -94,7 +94,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U Add the following to the .htaccess file in the root directory: - RedirectMatch "^\/\.well-known(.*)$" "\/blog\/\.well-known$1" + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1" Where 'blog' is the path to the subdirectory at which your blog resides. From ec4e22f57084178f8f7ae804c4066debb6a6fe84 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 13:34:14 +0200 Subject: [PATCH 200/427] fix routing checks --- includes/rest/class-server.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 8962db8..7e52772 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -88,11 +88,11 @@ class Server { public static function authorize_activitypub_requests( $response, $handler, $request ) { $route = $request->get_route(); - if ( ! str_starts_with( $route, '/activitypub' ) ) { - return $response; - } - - if ( get_rest_url_by_path( 'webfinger' ) !== $route ) { + if ( + ! str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) || + str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || + str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) + ) { return $response; } From d2b7c287fcd07d7876af8db9a525b7625e0be61b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 13:35:46 +0200 Subject: [PATCH 201/427] code doc --- includes/rest/class-server.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 7e52772..351284d 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -88,6 +88,7 @@ class Server { public static function authorize_activitypub_requests( $response, $handler, $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 ) || str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || @@ -96,12 +97,13 @@ class Server { return $response; } + // POST-Requets are always signed if ( 'POST' === $request->get_method() ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { return $verified_request; } - } elseif ( 'GET' === $request->get_method() ) { + } elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode if ( ACTIVITYPUB_SECURE_MODE ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { From 653b1f9fae989aa2ca3d3fff9569c83a44b076e1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 13:38:44 +0200 Subject: [PATCH 202/427] added missing `$2` see #339 --- README.md | 2 +- readme.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e434e69..3a3a360 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U Add the following to the .htaccess file in the root directory: - RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1" + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1$2" Where 'blog' is the path to the subdirectory at which your blog resides. diff --git a/readme.txt b/readme.txt index 85a02e8..0d2727b 100644 --- a/readme.txt +++ b/readme.txt @@ -94,7 +94,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U Add the following to the .htaccess file in the root directory: - RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1" + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1$2" Where 'blog' is the path to the subdirectory at which your blog resides. From 2c7f0687cc88c236defe00ede0eee7615ec44e6d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 22 May 2023 14:47:20 +0200 Subject: [PATCH 203/427] fix #271 thanks @janboddez --- templates/settings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 2bfaed4..0daca17 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -120,8 +120,8 @@

  • - name, $support_post_types, true ) ); ?> /> - + name, $support_post_types, true ) ); ?> /> +
  • From 677d507fe9f6512dc7083e0def8a9893568b5f21 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 11:10:05 +0200 Subject: [PATCH 204/427] Revorked "sanitize output" This reverts commit 77873d12b3ed55bf66536d081a2a0f20a52009ec. --- includes/class-shortcodes.php | 63 +++++++++++++++++++++++------------ includes/class-webfinger.php | 2 +- includes/model/class-post.php | 10 ++++-- 3 files changed, 49 insertions(+), 26 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index f0f8e22..7a0dd64 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -43,7 +43,7 @@ class Shortcodes { $hash_tags[] = \sprintf( '', \esc_url( \get_tag_link( $tag ) ), - \esc_html( $tag->slug ) + \wp_strip_all_tags( $tag->slug ) ); } @@ -66,7 +66,7 @@ class Shortcodes { return ''; } - return \esc_html( \get_the_title( $post_id ) ); + return \wp_strip_all_tags( \get_the_title( $post_id ), true ); } @@ -170,7 +170,7 @@ class Shortcodes { } } - return $excerpt; + return \apply_filters( 'the_excerpt', $excerpt ); } /** @@ -183,23 +183,36 @@ class Shortcodes { * @return string */ public static function content( $atts, $content, $tag ) { + // prevent inception + remove_shortcode( 'ap_content' ); + $post = get_post(); if ( ! $post || \post_password_required( $post ) ) { return ''; } + $atts = shortcode_atts( + array( 'apply_filters' => 'yes' ), + $atts, + $tag + ); + $content = \get_post_field( 'post_content', $post ); - $content = do_blocks( $content ); - $content = wptexturize( $content ); - $content = wp_filter_content_tags( $content ); - - // replace script and style elements - $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); + if ( 'yes' === $atts['apply_filters'] ) { + $content = \apply_filters( 'the_content', $content ); + } else { + $content = do_blocks( $content ); + $content = wptexturize( $content ); + $content = wp_filter_content_tags( $content ); + } + $content = strip_shortcodes( $content ); $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) ); + return $content; } @@ -231,7 +244,10 @@ class Shortcodes { return \esc_url( \get_permalink( $post->ID ) ); } - return \sprintf( '%1$s', \esc_url( \get_permalink( $post->ID ) ) ); + return \sprintf( + '%1$s', + \esc_url( \get_permalink( $post->ID ) ) + ); } /** @@ -262,7 +278,10 @@ class Shortcodes { return \esc_url( \wp_get_shortlink( $post->ID ) ); } - return \sprintf( '%1$s', \esc_url( \wp_get_shortlink( $post->ID ) ) ); + return \sprintf( + '%1$s', + \esc_url( \wp_get_shortlink( $post->ID ) ) + ); } /** @@ -336,7 +355,7 @@ class Shortcodes { $hash_tags[] = \sprintf( '', \esc_url( \get_category_link( $category ) ), - \esc_html( $category->slug ) + \wp_strip_all_tags( $category->slug ) ); } @@ -359,13 +378,13 @@ class Shortcodes { return ''; } - $name = \esc_html( \get_the_author_meta( 'display_name', $post->post_author ) ); + $name = \get_the_author_meta( 'display_name', $post->post_author ); if ( ! $name ) { return ''; } - return \esc_html( $name ); + return wp_strip_all_tags( $name ); } /** @@ -416,7 +435,7 @@ class Shortcodes { * @return string */ public static function blogname( $atts, $content, $tag ) { - return \esc_html( \get_bloginfo( 'name' ) ); + return \wp_strip_all_tags( \get_bloginfo( 'name' ) ); } /** @@ -429,7 +448,7 @@ class Shortcodes { * @return string */ public static function blogdesc( $atts, $content, $tag ) { - return \esc_html( \get_bloginfo( 'description' ) ); + return \wp_strip_all_tags( \get_bloginfo( 'description' ) ); } /** @@ -458,7 +477,7 @@ class Shortcodes { return ''; } - return \esc_html( $date ); + return $date; } /** @@ -487,15 +506,15 @@ class Shortcodes { return ''; } - return \esc_html( $date ); + return $date; } /** * Generates output for the ap_datetime shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts shortcode attributes + * @param string $content shortcode content + * @param string $tag shortcode tag name * * @return string */ @@ -516,6 +535,6 @@ class Shortcodes { return ''; } - return \esc_html( $date ); + return $date; } } diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 679f8a1..1581853 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -51,7 +51,7 @@ class Webfinger { } // try to access author URL - $response = \wp_safe_remote_get( + $response = \wp_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ), diff --git a/includes/model/class-post.php b/includes/model/class-post.php index ccb161c..0c36f58 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -508,8 +508,12 @@ class Post { $content = do_shortcode( $content ); wp_reset_postdata(); - $content = \wpautop( \wp_kses( $content, $this->allowed_tags ) ); - $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + // replace script and style elements + $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); + $content = \wp_kses( $content, $this->allowed_tags ); + $content = \wpautop( $content ); + $content = \preg_replace( '/[\n\r\t]/', '', $content ); + $content = \trim( $content ); $content = \apply_filters( 'activitypub_the_content', $content, $post ); $content = \html_entity_decode( $content, \ENT_QUOTES, 'UTF-8' ); @@ -537,6 +541,6 @@ class Post { return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]"; } - return $content; + return \get_option( 'activitypub_custom_post_content' ); } } From 3d1a0af6cb131045e94b776e2099ad6a5d2b8a8d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 11:13:17 +0200 Subject: [PATCH 205/427] moved strip style/script --- includes/class-shortcodes.php | 2 ++ includes/model/class-post.php | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 7a0dd64..5387387 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -208,6 +208,8 @@ class Shortcodes { $content = wp_filter_content_tags( $content ); } + // replace script and style elements + $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); $content = strip_shortcodes( $content ); $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 0c36f58..9d58068 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -508,8 +508,6 @@ class Post { $content = do_shortcode( $content ); wp_reset_postdata(); - // replace script and style elements - $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); $content = \wp_kses( $content, $this->allowed_tags ); $content = \wpautop( $content ); $content = \preg_replace( '/[\n\r\t]/', '', $content ); From d91eaeae725e94de9b1a8fb8283db3b819decdd3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 11:26:12 +0200 Subject: [PATCH 206/427] phpdoc --- includes/class-shortcodes.php | 160 +++++++++++++++++----------------- 1 file changed, 79 insertions(+), 81 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 5387387..010ef6d 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -3,9 +3,7 @@ namespace Activitypub; class Shortcodes { /** - * Class constructor, registering WordPress then shortcodes - * - * @param WP_Post $post A WordPress Post Object + * Class constructor, registering WordPress then Shortcodes */ public static function init() { foreach ( get_class_methods( 'Activitypub\Shortcodes' ) as $shortcode ) { @@ -16,13 +14,13 @@ class Shortcodes { } /** - * Generates output for the ap_hashtags shortcode + * Generates output for the 'ap_hashtags' shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post tags as hashtags. */ public static function hashtags( $atts, $content, $tag ) { $post_id = get_the_ID(); @@ -51,13 +49,13 @@ class Shortcodes { } /** - * Generates output for the ap_title shortcode + * Generates output for the 'ap_title' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post title. */ public static function title( $atts, $content, $tag ) { $post_id = get_the_ID(); @@ -71,13 +69,13 @@ class Shortcodes { } /** - * Generates output for the ap_excerpt shortcode + * Generates output for the 'ap_excerpt' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post excerpt. */ public static function excerpt( $atts, $content, $tag ) { $post = get_post(); @@ -174,13 +172,13 @@ class Shortcodes { } /** - * Generates output for the ap_content shortcode + * Generates output for the 'ap_content' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post content. */ public static function content( $atts, $content, $tag ) { // prevent inception @@ -219,13 +217,13 @@ class Shortcodes { } /** - * Generates output for the ap_permalink shortcode + * Generates output for the 'ap_permalink' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post permalink. */ public static function permalink( $atts, $content, $tag ) { $post = get_post(); @@ -253,13 +251,13 @@ class Shortcodes { } /** - * Generates output for the ap_shortlink shortcode + * Generates output for the 'ap_shortlink' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post shortlink. */ public static function shortlink( $atts, $content, $tag ) { $post = get_post(); @@ -287,11 +285,11 @@ class Shortcodes { } /** - * Generates output for the ap_image shortcode + * Generates output for the 'ap_image' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * * @return string */ @@ -330,13 +328,13 @@ class Shortcodes { } /** - * Generates output for the ap_hashcats shortcode + * Generates output for the 'ap_hashcats' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post categories as hashtags. */ public static function hashcats( $atts, $content, $tag ) { $post_id = get_the_ID(); @@ -365,13 +363,13 @@ class Shortcodes { } /** - * Generates output for the ap_author shortcode + * Generates output for the 'ap_author' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The author name. */ public static function author( $atts, $content, $tag ) { $post = get_post(); @@ -390,13 +388,13 @@ class Shortcodes { } /** - * Generates output for the ap_authorurl shortcode + * Generates output for the 'ap_authorurl' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The author URL. */ public static function authorurl( $atts, $content, $tag ) { $post = get_post(); @@ -415,24 +413,24 @@ class Shortcodes { } /** - * Generates output for the ap_blogurl shortcode + * Generates output for the 'ap_blogurl' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The site URL. */ public static function blogurl( $atts, $content, $tag ) { return \esc_url( \get_bloginfo( 'url' ) ); } /** - * Generates output for the ap_blogname shortcode + * Generates output for the 'ap_blogname' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * * @return string */ @@ -441,26 +439,26 @@ class Shortcodes { } /** - * Generates output for the ap_blogdesc shortcode + * Generates output for the 'ap_blogdesc' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The site description. */ public static function blogdesc( $atts, $content, $tag ) { return \wp_strip_all_tags( \get_bloginfo( 'description' ) ); } /** - * Generates output for the ap_date shortcode + * Generates output for the 'ap_date' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post date. */ public static function date( $atts, $content, $tag ) { $post = get_post(); @@ -483,13 +481,13 @@ class Shortcodes { } /** - * Generates output for the ap_time shortcode + * Generates output for the 'ap_time' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post time. */ public static function time( $atts, $content, $tag ) { $post = get_post(); @@ -512,13 +510,13 @@ class Shortcodes { } /** - * Generates output for the ap_datetime shortcode + * Generates output for the 'ap_datetime' Shortcode * - * @param array $atts shortcode attributes - * @param string $content shortcode content - * @param string $tag shortcode tag name + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. * - * @return string + * @return string The post date/time. */ public static function datetime( $atts, $content, $tag ) { $post = get_post(); From 83991c0cd83e56c34e5937fcf8fb88381ed5c319 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 12:14:39 +0200 Subject: [PATCH 207/427] fix #332 and some of the feedback of @mattwiebe --- includes/help.php | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/includes/help.php b/includes/help.php index ccf61cc..291162c 100644 --- a/includes/help.php +++ b/includes/help.php @@ -8,37 +8,37 @@ '

    ' . __( 'The following Template Tags are available:', 'activitypub' ) . '

    ' . '
    ' . '
    [ap_title]
    ' . - '
    ' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), 'default' ) . '
    ' . - '
    [ap_content]
    ' . - '
    ' . \wp_kses( __( 'The post\'s content.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . + '
    [ap_content apply_filters="yes"]
    ' . + '
    ' . \wp_kses( __( 'The post\'s content. With apply_filters you can decide if filters (apply_filters( \'the_content\', $content )) should be applied or not (default is yes). The values can be yes or no. apply_filters attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_excerpt lenght="400"]
    ' . - '
    ' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). length attribute is optional.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). length attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_permalink type="url"]
    ' . - '
    ' . \wp_kses( __( 'The post\'s permalink. type can be either: url or html (an <a /> tag). type attribute is optional.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s permalink. type can be either: url or html (an <a /> tag). type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_shortlink type="url"]
    ' . - '
    ' . \wp_kses( __( 'The post\'s shortlink. type can be either url or html (an <a /> tag). I can recommend Hum, to prettify the Shortlinks. type attribute is optional.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s shortlink. type can be either url or html (an <a /> tag). I can recommend Hum, to prettify the Shortlinks. type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_hashtags]
    ' . - '
    ' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_hashcats]
    ' . - '
    ' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_image type=full]
    ' . - '
    ' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: thumbnail, medium, large, full. type attribute is optional.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: thumbnail, medium, large, full. type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_author]
    ' . - '
    ' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_authorurl]
    ' . - '
    ' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_date]
    ' . - '
    ' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_time]
    ' . - '
    ' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_datetime]
    ' . - '
    ' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_blogurl]
    ' . - '
    ' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_blogname]
    ' . - '
    ' . \wp_kses( __( 'The name of the site.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The name of the site.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    [ap_blogdesc]
    ' . - '
    ' . \wp_kses( __( 'The description of the site.', 'activitypub' ), 'default' ) . '
    ' . + '
    ' . \wp_kses( __( 'The description of the site.', 'activitypub' ), array( 'code' => array() ) ) . '
    ' . '
    ' . '

    ' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '

    ' . '

    ' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '

    ' . @@ -48,8 +48,8 @@ \get_current_screen()->add_help_tab( array( - 'id' => 'glossar', - 'title' => \__( 'Glossar', 'activitypub' ), + 'id' => 'glossary', + 'title' => \__( 'Glossary', 'activitypub' ), 'content' => '

    ' . \__( 'Fediverse', 'activitypub' ) . '

    ' . '

    ' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '

    ' . From 2aa7077ae750f3203cf5dcd6d6835aea314dd7fc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 12:26:02 +0200 Subject: [PATCH 208/427] add `wpautop` to user description fix #279 --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 5bcf97a..2337a7c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -205,7 +205,7 @@ function get_author_description( $user_id ) { if ( empty( $description ) ) { $description = get_user_meta( $user_id, 'description', true ); } - return $description; + return \wpautop( \wp_kses( $description, 'default' ) ); } /** From 2117f78106f075d647695bcb576f28f99bcbeb2f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 12:28:57 +0200 Subject: [PATCH 209/427] fix #321 --- includes/model/class-post.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index ff5ccb8..87d8dec 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -95,8 +95,13 @@ class Post { 'class' => array(), ), 'ul' => array(), - 'ol' => array(), - 'li' => array(), + 'ol' => array( + 'reversed' => array(), + 'start' => array(), + ), + 'li' => array( + 'value' => array(), + ), 'strong' => array( 'class' => array(), ), From a9b964a087fe33c1a5d2cd0f8823af696ab80937 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 13:44:08 +0200 Subject: [PATCH 210/427] fix #237 --- templates/welcome.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/templates/welcome.php b/templates/welcome.php index 2b6a362..cb4d05f 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -50,6 +50,7 @@

    +

    -

    +

    + +

    + +

    From bc54598828105d6793ef0a72c54dcfe7a13cbc89 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 23 May 2023 19:10:30 +0200 Subject: [PATCH 211/427] Fix CI and added Calckey --- README.md | 7 ++++--- readme.txt | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3a3a360..8264b83 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,12 @@ The plugin works with the following tested federated platforms, but there may be * [Mastodon](https://joinmastodon.org/) * [Pleroma](https://pleroma.social/) -* [Friendica](https://friendi.ca/) -* [HubZilla](https://hubzilla.org/) +* [friendica](https://friendi.ca/) +* [Hubzilla](https://hubzilla.org/) * [Pixelfed](https://pixelfed.org/) -* [SocialHome](https://socialhome.network/) +* [Socialhome](https://socialhome.network/) * [Misskey](https://join.misskey.page/) +* [Calckey](https://calckey.org/) Here’s what that means and what you can expect. diff --git a/readme.txt b/readme.txt index 0d2727b..13ba977 100644 --- a/readme.txt +++ b/readme.txt @@ -20,11 +20,12 @@ The plugin works with the following tested federated platforms, but there may be * [Mastodon](https://joinmastodon.org/) * [Pleroma](https://pleroma.social/) -* [Friendica](https://friendi.ca/) -* [HubZilla](https://hubzilla.org/) +* [friendica](https://friendi.ca/) +* [Hubzilla](https://hubzilla.org/) * [Pixelfed](https://pixelfed.org/) -* [SocialHome](https://socialhome.network/) +* [Socialhome](https://socialhome.network/) * [Misskey](https://join.misskey.page/) +* [Calckey](https://calckey.org/) Here’s what that means and what you can expect. From 221c5778264457e050adc5d2208dab2d6ae85db5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 14:03:30 +0200 Subject: [PATCH 212/427] Fix federation with pixelfed! --- includes/class-signature.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0ef8bcb..5ab47cc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -353,6 +353,12 @@ class Signature { if ( 'content-length' === $header ) { $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; } + if ( 'user-agent' === $header ) { + $signed_data .= $header . ': ' . $headers['user_agent'][0] . "\n"; + } + if ( 'accept' === $header ) { + $signed_data .= $header . ': ' . $headers['accept'][0] . "\n"; + } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. $d = new DateTime( $headers[ $header ][0] ); From 273493e7689e349bf0779b53d77d2d515a39cd12 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Fri, 26 May 2023 12:40:46 -0600 Subject: [PATCH 213/427] update header parsing in get_signed_data() --- includes/class-signature.php | 36 +++++++++++++----------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 5ab47cc..64467b7 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -322,42 +322,31 @@ class Signature { $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 ( \array_key_exists( $header, $headers ) ) { - if ( 'host' === $header ) { - if ( isset( $headers['x_original_host'] ) ) { - $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; - } else { - $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; - } - } else { - $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + if ( 'host' === $header ) { + if ( isset( $headers['x_original_host'] ) ) { + $signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n"; + continue; } } + if ( '(request-target)' === $header ) { + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + continue; + } + if ( str_contains( $header, '-' ) ) { + $signed_data .= $header . ': ' . $headers[ str_replace( '-', '_', $header ) ][0] . "\n"; + continue; + } if ( '(created)' === $header ) { if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) { // created in future return false; } - $signed_data .= '(created): ' . $signature_block['(created)'] . "\n"; } if ( '(expires)' === $header ) { if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) { // expired in past return false; } - $signed_data .= '(expires): ' . $signature_block['(expires)'] . "\n"; - } - if ( 'content-type' === $header ) { - $signed_data .= $header . ': ' . $headers['content_type'][0] . "\n"; - } - if ( 'content-length' === $header ) { - $signed_data .= $header . ': ' . $headers['content_length'][0] . "\n"; - } - if ( 'user-agent' === $header ) { - $signed_data .= $header . ': ' . $headers['user_agent'][0] . "\n"; - } - if ( 'accept' === $header ) { - $signed_data .= $header . ': ' . $headers['accept'][0] . "\n"; } if ( 'date' === $header ) { // allow a bit of leeway for misconfigured clocks. @@ -373,6 +362,7 @@ class Signature { return false; } } + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; } return \rtrim( $signed_data, "\n" ); } From ab0f48389c99bec1477bead2489771d47494da99 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 10:47:49 +0200 Subject: [PATCH 214/427] deregister schedules on uninstall --- includes/class-activitypub.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index c5d0e71..710038e 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -56,6 +56,7 @@ class Activitypub { * @return void */ public static function uninstall() { + Scheduler::deregister_schedules(); } /** From c04cf3fc7e72529f49fef3a0060bec82b284b71c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 10:48:06 +0200 Subject: [PATCH 215/427] move schedule to scheduler-class --- includes/class-admin.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index fe44ed4..7b62a08 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -15,7 +15,6 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); - \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); @@ -146,12 +145,6 @@ class Admin { ); } - public static function schedule_migration() { - if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) ) { - \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); - } - } - public static function add_settings_help_tab() { require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php'; } From 084f10816195bb5bc49fa15d5140c512c5824a17 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 10:48:51 +0200 Subject: [PATCH 216/427] only schedule migration if DB is not on the latest version --- includes/class-scheduler.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index a79044e..07926d8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -19,6 +19,8 @@ class Scheduler { \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); + + \add_action( 'admin_init', array( self::class, 'schedule_migration' ) ); } /** @@ -135,4 +137,15 @@ class Scheduler { } } } + + /** + * Schedule migration if DB-Version is not up to date. + * + * @return void + */ + public static function schedule_migration() { + if ( ! \wp_next_scheduled( 'activitypub_schedule_migration' ) && ! Migration::is_latest_version() ) { + \wp_schedule_single_event( \time(), 'activitypub_schedule_migration' ); + } + } } From 758912da64f37d4a028ab0adeb7f398ee5cf43c6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 14:03:46 +0200 Subject: [PATCH 217/427] do not use cache for new followers --- includes/class-mention.php | 2 +- includes/class-scheduler.php | 4 ++-- includes/functions.php | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/includes/class-mention.php b/includes/class-mention.php index 49f7860..a072bde 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -114,7 +114,7 @@ class Mention { * @return string The Inbox-URL */ public static function get_inbox_by_mentioned_actor( $actor ) { - $metadata = get_remote_metadata_by_actor( $actor, true ); + $metadata = get_remote_metadata_by_actor( $actor ); if ( \is_wp_error( $metadata ) ) { return $metadata; diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 07926d8..ea1a6e4 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -100,7 +100,7 @@ class Scheduler { $followers = Followers::get_outdated_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { $follower->set_error( $meta ); @@ -121,7 +121,7 @@ class Scheduler { $followers = Followers::get_faulty_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor() ); + $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); if ( is_tombstone( $meta ) ) { $follower->delete(); diff --git a/includes/functions.php b/includes/functions.php index 2337a7c..423a6ef 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -54,11 +54,12 @@ function get_webfinger_resource( $user_id ) { /** * Requests the Meta-Data from the Actors profile * - * @param string $actor The Actor URL + * @param string $actor The Actor URL. + * @param bool $cached If the result should be cached. * * @return array The Actor profile as array */ -function get_remote_metadata_by_actor( $actor ) { +function get_remote_metadata_by_actor( $actor, $cached = true ) { $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); if ( $pre ) { return $pre; @@ -75,11 +76,14 @@ function get_remote_metadata_by_actor( $actor ) { return $actor; } - $transient_key = 'activitypub_' . $actor; - $metadata = \get_transient( $transient_key ); + // only check the cache if needed. + if ( $cached ) { + $transient_key = 'activitypub_' . $actor; + $metadata = \get_transient( $transient_key ); - if ( $metadata ) { - return $metadata; + if ( $metadata ) { + return $metadata; + } } if ( ! \wp_http_validate_url( $actor ) ) { From 285925ea08740c27c0d68ab713fb3ac72f1865a8 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 31 May 2023 06:35:58 -0600 Subject: [PATCH 218/427] test_activity_signature --- ...typub-rest-post-signature-verification.php | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/test-class-activitypub-rest-post-signature-verification.php diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php new file mode 100644 index 0000000..f972871 --- /dev/null +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -0,0 +1,57 @@ + 1, + 'post_content' => 'hello world', + ) + ); + $remote_actor = \get_author_posts_url( 2 ); + $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); + $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity->add_cc( $remote_actor ); + $activity = $activitypub_activity->to_json(); + + // generate_digest & generate_signature + $digest = Activitypub\Signature::generate_digest( $activity ); + $date = gmdate( 'D, d M Y H:i:s T' ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + + // Signed headers + $url_parts = wp_parse_url( $remote_actor ); + $route = $url_parts['path'] . '?' . $url_parts['query']; + $host = $url_parts['host']; + + $headers = array( + 'digest' => [ "SHA-256=$digest" ], + 'signature' => [ $signature ], + 'date' => [ $date ], + 'host' => [ $host ], + '(request-target)' => [ 'POST ' . $route ] + ); + + // Start verification + // parse_signature_header, get_signed_data, get_public_key + $signature_block = Activitypub\Signature::parse_signature_header( $headers['signature'] ); + $signed_headers = $signature_block['headers']; + $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); + $public_key = Activitypub\Signature::get_public_key( 1 ); + + // signature_verification + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; + $this->assertTRUE( $verified ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + + +} From 73cd19ec207a48c16390b083607f692471715338 Mon Sep 17 00:00:00 2001 From: Django Doucet Date: Wed, 31 May 2023 23:23:40 -0600 Subject: [PATCH 219/427] added test and pre_get_remote_key filter --- includes/class-signature.php | 5 ++ ...typub-rest-post-signature-verification.php | 87 ++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 64467b7..2c5be60 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -242,6 +242,11 @@ class Signature { * @return string $publicKeyPem */ public static function get_remote_key( $key_id ) { // phpcs:ignore + $pre = apply_filters( 'pre_get_remote_key', false, $key_id ); + if ( $pre ) { + return $pre; + } + $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f972871..a0c5e08 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -1,6 +1,35 @@ server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + } + + /** + * Tear down after test ends + */ + public function tearDown() : void { + remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); + parent::tearDown(); + + global $wp_rest_server; + $wp_rest_server = null; + + } public function test_activity_signature() { @@ -53,5 +82,61 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_rest_activity_signature() { + + $pre_http_request = new MockAction(); + // $pre_get_remote_key = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + add_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); + + // Activity Object + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello world', + ) + ); + $remote_actor = \get_author_posts_url( 2 ); + $remote_actor_inbox = \get_rest_url( null, 'activitypub/1.0/inbox' ); + $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); + $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity->add_cc( $remote_actor_inbox ); + $activity = $activitypub_activity->to_json(); + + // generate_digest & generate_signature + $digest = Activitypub\Signature::generate_digest( $activity ); + $date = gmdate( 'D, d M Y H:i:s T' ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + + // Signed headers + $url_parts = wp_parse_url( $remote_actor ); + $route = add_query_arg( $url_parts['query'], $url_parts['path'] ); + $host = $url_parts['host']; + + $request = new WP_REST_Request( 'POST', ACTIVITYPUB_REST_NAMESPACE . '/inbox' ); + $request->set_header( 'content-type', 'application/activity+json' ); + $request->set_header( 'digest', "SHA-256=$digest" ); + $request->set_header( 'signature', $signature ); + $request->set_header( 'date', $date ); + $request->set_header( 'host', $host ); + $request->set_body( $activity ); + + // Start verification + $verified = \Activitypub\Signature::verify_http_signature( $request ); + // $this->assertTRUE( $verified ); + + remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + + public static function pre_get_remote_key( $pre, $key_id ) { + $query = wp_parse_url( $key_id, PHP_URL_QUERY ); + parse_str( $query, $output ); + if ( is_int( $output['author'] ) ) { + return ActivityPub\Signature::get_public_key( int( $output['author'] ) ); + } + return $pre; + } } From 727aaf1c4578861fe171784d9cea9ff7647afcd8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 08:05:19 +0200 Subject: [PATCH 220/427] add signature regex test --- tests/test-class-activitypub-activity.php | 2 +- ...est-class-activitypub-rest-post-signature-verification.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index af0ed55..d689677 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); - $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); + $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); remove_all_filters( 'activitypub_extract_mentions' ); diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f972871..e9335fa 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -26,6 +26,8 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase $date = gmdate( 'D, d M Y H:i:s T' ); $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + $this->assertRegExp( '/keyId="http:\/\/example\.org\/\?author=1#main-key",algorithm="rsa-sha256",headers="\(request-target\) host date digest",signature="[^"]*"/', $signature ); + // Signed headers $url_parts = wp_parse_url( $remote_actor ); $route = $url_parts['path'] . '?' . $url_parts['query']; @@ -48,7 +50,7 @@ class Test_Activitypub_Rest_Post_Signature_Verification extends WP_UnitTestCase // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; - $this->assertTRUE( $verified ); + $this->assertTrue( $verified ); remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } From 96881b940afdadd2088cea7f134ffcb6b513a7fc Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 09:49:40 +0200 Subject: [PATCH 221/427] some refactorings and fixed the tests --- includes/class-http.php | 2 +- includes/class-signature.php | 15 ++- ...typub-rest-post-signature-verification.php | 96 ++++++------------- 3 files changed, 39 insertions(+), 74 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 58cace9..58551a4 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -34,7 +34,7 @@ class Http { 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Digest' => "SHA-256=$digest", + 'Digest' => $digest, 'Signature' => $signature, 'Date' => $date, ), diff --git a/includes/class-signature.php b/includes/class-signature.php index 2c5be60..e06a226 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -131,8 +131,10 @@ class Signature { $path .= '?' . $url_parts['query']; } + $http_method = \strtolower( $http_method ); + if ( ! empty( $digest ) ) { - $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest"; + $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: $digest"; } else { $signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date"; } @@ -165,7 +167,7 @@ class Signature { if ( is_object( $request ) ) { // REST Request object $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' /' . rest_get_url_prefix() . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $request->get_route(); } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array @@ -227,7 +229,9 @@ class Signature { if ( \is_wp_error( $public_key ) ) { return $public_key; } + $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; + if ( ! $verified ) { return new WP_Error( 'activitypub_signature', 'Invalid signature', array( 'status' => 403 ) ); } @@ -242,11 +246,6 @@ class Signature { * @return string $publicKeyPem */ public static function get_remote_key( $key_id ) { // phpcs:ignore - $pre = apply_filters( 'pre_get_remote_key', false, $key_id ); - if ( $pre ) { - return $pre; - } - $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; @@ -374,7 +373,7 @@ class Signature { public static function generate_digest( $body ) { $digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore - return "$digest"; + return "SHA-256=$digest"; } /** diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index e87b1fd..f0acd34 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -1,41 +1,7 @@ server = $wp_rest_server; - - do_action( 'rest_api_init' ); - - } - - /** - * Tear down after test ends - */ - public function tearDown() : void { - remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); - parent::tearDown(); - - global $wp_rest_server; - $wp_rest_server = null; - - } - public function test_activity_signature() { - - $pre_http_request = new MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - // Activity for generate_digest $post = \wp_insert_post( array( @@ -63,11 +29,11 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $host = $url_parts['host']; $headers = array( - 'digest' => [ "SHA-256=$digest" ], - 'signature' => [ $signature ], - 'date' => [ $date ], - 'host' => [ $host ], - '(request-target)' => [ 'POST ' . $route ] + 'digest' => array( $digest ), + 'signature' => array( $signature ), + 'date' => array( $date ), + 'host' => array( $host ), + '(request-target)' => array( 'post ' . $route ), ); // Start verification @@ -75,21 +41,32 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signature_block = Activitypub\Signature::parse_signature_header( $headers['signature'] ); $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); + $public_key = Activitypub\Signature::get_public_key( 1 ); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; $this->assertTrue( $verified ); - - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } public function test_rest_activity_signature() { - - $pre_http_request = new MockAction(); - // $pre_get_remote_key = new MockAction(); - add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - add_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); + add_filter( + 'pre_get_remote_metadata_by_actor', + function( $json, $actor ) { + // return ActivityPub Profile with signature + return array( + 'id' => $actor, + 'type' => 'Person', + 'publicKey' => array( + 'id' => $actor . '#main-key', + 'owner' => $actor, + 'publicKeyPem' => \Activitypub\Signature::get_public_key( 1 ), + ), + ); + }, + 10, + 2 + ); // Activity Object $post = \wp_insert_post( @@ -99,7 +76,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ) ); $remote_actor = \get_author_posts_url( 2 ); - $remote_actor_inbox = \get_rest_url( null, 'activitypub/1.0/inbox' ); + $remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' ); $activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); $activitypub_activity->from_post( $activitypub_post ); @@ -109,16 +86,16 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { // generate_digest & generate_signature $digest = Activitypub\Signature::generate_digest( $activity ); $date = gmdate( 'D, d M Y H:i:s T' ); - $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor, $date, $digest ); + $signature = Activitypub\Signature::generate_signature( 1, 'POST', $remote_actor_inbox, $date, $digest ); // Signed headers - $url_parts = wp_parse_url( $remote_actor ); - $route = add_query_arg( $url_parts['query'], $url_parts['path'] ); + $url_parts = wp_parse_url( $remote_actor_inbox ); + $route = $url_parts['path'] . '?' . $url_parts['query']; $host = $url_parts['host']; - $request = new WP_REST_Request( 'POST', ACTIVITYPUB_REST_NAMESPACE . '/inbox' ); + $request = new WP_REST_Request( 'POST', $route ); $request->set_header( 'content-type', 'application/activity+json' ); - $request->set_header( 'digest', "SHA-256=$digest" ); + $request->set_header( 'digest', $digest ); $request->set_header( 'signature', $signature ); $request->set_header( 'date', $date ); $request->set_header( 'host', $host ); @@ -126,19 +103,8 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { // Start verification $verified = \Activitypub\Signature::verify_http_signature( $request ); - // $this->assertTRUE( $verified ); + $this->assertTrue( $verified ); - remove_filter( 'pre_get_remote_key', array( get_called_class(), 'pre_get_remote_key' ) ); - remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_key' ), 10, 2 ); } - - public static function pre_get_remote_key( $pre, $key_id ) { - $query = wp_parse_url( $key_id, PHP_URL_QUERY ); - parse_str( $query, $output ); - if ( is_int( $output['author'] ) ) { - return ActivityPub\Signature::get_public_key( int( $output['author'] ) ); - } - return $pre; - } - } From c1bf6691c1b2686c0ea30dbd715db5ef42b188fd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:13:49 +0200 Subject: [PATCH 222/427] fix route issues --- includes/class-signature.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index e06a226..b4c9374 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -165,9 +165,15 @@ class Signature { */ public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object + // check if route starts with "index.php" + if ( str_starts_with( $request->get_route(), 'index.php' ) ) { + $route = $request->get_route(); + } else { + $route = rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); + } $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; - $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $request->get_route(); + $headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route; } else { $request = self::format_server_request( $request ); $headers = $request['headers']; // $_SERVER array From 9118e506234f3923e38f1e5a3d88098fd43bf06b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:25:18 +0200 Subject: [PATCH 223/427] fix signature verification path --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index b4c9374..0b5f681 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -169,7 +169,7 @@ class Signature { if ( str_starts_with( $request->get_route(), 'index.php' ) ) { $route = $request->get_route(); } else { - $route = rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); + $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); } $headers = $request->get_headers(); $actor = isset( json_decode( $request->get_body() )->actor ) ? json_decode( $request->get_body() )->actor : ''; From b834666edad2641e5a1b932ce699d3061b34c506 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 10:44:05 +0200 Subject: [PATCH 224/427] add missing slash --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 0b5f681..06ea766 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -166,7 +166,7 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object // check if route starts with "index.php" - if ( str_starts_with( $request->get_route(), 'index.php' ) ) { + if ( str_starts_with( $request->get_route(), '/index.php' ) ) { $route = $request->get_route(); } else { $route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' ); From 00dd5d2c52ab7198cdc7e5271e1c09d44f385b0b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:05:47 +0200 Subject: [PATCH 225/427] some phpdoc --- includes/class-signature.php | 45 +++++++++++++++++++++--------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 06ea766..a3293d6 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -247,9 +247,9 @@ class Signature { /** * Get public key from key_id * - * @param string $key_id + * @param string $key_id The URL to the public key. * - * @return string $publicKeyPem + * @return string The public key. */ public static function get_remote_key( $key_id ) { // phpcs:ignore $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore @@ -267,7 +267,7 @@ class Signature { * * @param array $signature_block * - * @return string algorithm + * @return string The signature algorithm. */ public static function get_signature_algorithm( $signature_block ) { if ( $signature_block['algorithm'] ) { @@ -284,39 +284,39 @@ class Signature { /** * Parses the Signature header * - * @param array $header + * @param array $header The signature header. * * @return array signature parts */ public static function parse_signature_header( $header ) { - $ret = array(); - $matches = array(); - $h_string = \implode( ',', (array) $header[0] ); + $parsed_header = array(); + $matches = array(); + $h_string = \implode( ',', (array) $header[0] ); if ( \preg_match( '/keyId="(.*?)"/ism', $h_string, $matches ) ) { - $ret['keyId'] = $matches[1]; + $parsed_header['keyId'] = $matches[1]; } if ( \preg_match( '/created=([0-9]*)/ism', $h_string, $matches ) ) { - $ret['(created)'] = $matches[1]; + $parsed_header['(created)'] = $matches[1]; } if ( \preg_match( '/expires=([0-9]*)/ism', $h_string, $matches ) ) { - $ret['(expires)'] = $matches[1]; + $parsed_header['(expires)'] = $matches[1]; } if ( \preg_match( '/algorithm="(.*?)"/ism', $h_string, $matches ) ) { - $ret['algorithm'] = $matches[1]; + $parsed_header['algorithm'] = $matches[1]; } if ( \preg_match( '/headers="(.*?)"/ism', $h_string, $matches ) ) { - $ret['headers'] = \explode( ' ', $matches[1] ); + $parsed_header['headers'] = \explode( ' ', $matches[1] ); } if ( \preg_match( '/signature="(.*?)"/ism', $h_string, $matches ) ) { - $ret['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore + $parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', $matches[1] ) ); // phpcs:ignore } - if ( ( $ret['signature'] ) && ( $ret['algorithm'] ) && ( ! $ret['headers'] ) ) { - $ret['headers'] = array( 'date' ); + if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) { + $parsed_header['headers'] = array( 'date' ); } - return $ret; + return $parsed_header; } /** @@ -326,7 +326,7 @@ class Signature { * @param array $signature_block (pseudo-headers) * @param array $headers (http headers) * - * @return signed headers for comparison + * @return string signed headers for comparison */ public static function get_signed_data( $signed_headers, $signature_block, $headers ) { $signed_data = ''; @@ -377,6 +377,13 @@ class Signature { return \rtrim( $signed_data, "\n" ); } + /** + * Generates the digest for a 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 return "SHA-256=$digest"; @@ -386,9 +393,9 @@ class Signature { * Formats the $_SERVER to resemble the WP_REST_REQUEST array, * for use with verify_http_signature() * - * @param array $_SERVER + * @param array $_SERVER The $_SERVER array. * - * @return array $request + * @return array $request The formatted request array. */ public static function format_server_request( $server ) { $request = array(); From 00e56ca112bda422e9e646ad7871b11228b5d615 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:17:08 +0200 Subject: [PATCH 226/427] always use is_activitypub_request to check if it is an AP request --- includes/class-activitypub.php | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 3b16213..e99bcab 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -88,27 +88,7 @@ class Activitypub { $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php'; } - global $wp_query; - - if ( isset( $wp_query->query_vars['activitypub'] ) ) { - return $json_template; - } - - if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) { - return $template; - } - - $accept_header = $_SERVER['HTTP_ACCEPT']; - // Accept header as an array. - $accept = \explode( ',', \trim( $accept_header ) ); - if ( - \stristr( $accept_header, 'application/activity+json' ) || - \stristr( $accept_header, 'application/ld+json' ) || - \in_array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', $accept, true ) || - \in_array( 'application/activity+json', $accept, true ) || - \in_array( 'application/ld+json', $accept, true ) || - \in_array( 'application/json', $accept, true ) - ) { + if ( is_activitypub_request() ) { if ( ACTIVITYPUB_SECURE_MODE ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { From f11cf7c1f07bd44cad016947589f38478d47ae11 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 14:50:55 +0200 Subject: [PATCH 227/427] update readme --- README.md | 2 ++ readme.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 8264b83..b8643fe 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu ### Next ### +* Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . +* Update: Complete rewrite of the Follower-System based on Taxonomies. * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. diff --git a/readme.txt b/readme.txt index 13ba977..8d1bc99 100644 --- a/readme.txt +++ b/readme.txt @@ -116,6 +116,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu = Next = +* Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . +* Update: Complete rewrite of the Follower-System based on Taxonomies. * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. From ba7f57d6ff346eddba2c3f861c969542227b5fff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 03:03:40 +0000 Subject: [PATCH 228/427] Update yoast/phpunit-polyfills requirement from ^1.0 to ^2.0 Updates the requirements on [yoast/phpunit-polyfills](https://github.com/Yoast/PHPUnit-Polyfills) to permit the latest version. - [Release notes](https://github.com/Yoast/PHPUnit-Polyfills/releases) - [Changelog](https://github.com/Yoast/PHPUnit-Polyfills/blob/2.x/CHANGELOG.md) - [Commits](https://github.com/Yoast/PHPUnit-Polyfills/compare/1.0.0...2.0.0) --- updated-dependencies: - dependency-name: yoast/phpunit-polyfills dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b0117c4..68cefb9 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "phpcompatibility/phpcompatibility-wp": "*", "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "*", - "yoast/phpunit-polyfills": "^1.0", + "yoast/phpunit-polyfills": "^2.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0" }, "config": { From 87de87b2a587ba5eed65b047a85f7d9579c47e32 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Mon, 12 Jun 2023 11:38:15 -0500 Subject: [PATCH 229/427] Followers: use custom post types and postmeta to store --- includes/collection/class-followers.php | 185 ++++++++++-------------- includes/model/class-follower.php | 126 +++++++++------- 2 files changed, 154 insertions(+), 157 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 142a9a1..7217666 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -15,10 +15,11 @@ use function Activitypub\get_remote_metadata_by_actor; /** * ActivityPub Followers Collection * + * @author Matt Wiebe * @author Matthias Pfefferle */ class Followers { - const TAXONOMY = 'activitypub-followers'; + const POST_TYPE = 'activitypub_followers'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; /** @@ -27,8 +28,8 @@ class Followers { * @return void */ public static function init() { - // register "followers" taxonomy - self::register_taxonomy(); + // register "followers" post_type + self::register_post_type(); \add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 ); \add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 ); @@ -41,31 +42,26 @@ class Followers { * * @return void */ - public static function register_taxonomy() { - $args = array( - 'labels' => array( - 'name' => _x( 'Followers', 'taxonomy general name', 'activitypub' ), - 'singular_name' => _x( 'Followers', 'taxonomy singular name', 'activitypub' ), - 'menu_name' => __( 'Followers', 'activitypub' ), - ), - 'hierarchical' => false, - 'show_ui' => false, - 'show_in_menu' => false, - 'show_in_nav_menus' => false, - 'show_admin_column' => false, - 'query_var' => false, - 'rewrite' => false, - 'public' => false, - 'capabilities' => array( - 'edit_terms' => null, - ), + private static function register_post_type() { + register_post_type( + self::POST_TYPE, + array( + 'labels' => array( + 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) ); - register_taxonomy( self::TAXONOMY, 'user', $args ); - register_taxonomy_for_object_type( self::TAXONOMY, 'user' ); - - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'name', array( 'type' => 'string', @@ -76,8 +72,8 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'username', array( 'type' => 'string', @@ -88,56 +84,48 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'avatar', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - return ''; - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, + 'url', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), + ) + ); + + register_post_meta( + self::POST_TYPE, 'inbox', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - throw new Exception( '"inbox" has to be a valid URL' ); - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'shared_inbox', array( 'type' => 'string', 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - return null; - } - - return esc_url_raw( $value ); - }, + 'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'updated_at', array( 'type' => 'string', @@ -152,8 +140,8 @@ class Followers { ) ); - register_term_meta( - self::TAXONOMY, + register_post_meta( + self::POST_TYPE, 'errors', array( 'type' => 'string', @@ -168,7 +156,15 @@ class Followers { ) ); - do_action( 'activitypub_after_register_taxonomy' ); + do_action( 'activitypub_after_register_post_type' ); + } + + public static function sanitize_url( $value ) { + if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { + return null; + } + + return esc_url_raw( $value ); } /** @@ -217,12 +213,10 @@ class Followers { return $meta; } - $follower = new Follower( $actor ); + $follower = new Follower( $actor, $user_id ); $follower->from_meta( $meta ); $follower->upsert(); - $result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); - if ( is_wp_error( $result ) ) { return $result; } else { @@ -241,7 +235,7 @@ class Followers { */ public static function remove_follower( $user_id, $actor ) { wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); - return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); + return wp_remove_object_terms( $user_id, $actor, self::POST_TYPE ); } /** @@ -253,24 +247,8 @@ class Followers { * @return \Activitypub\Model\Follower The Follower object */ public static function get_follower( $user_id, $actor ) { - $terms = new WP_Term_Query( - array( - 'name' => $actor, - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, - 'number' => 1, - ) - ); - - $term = $terms->get_terms(); - - if ( is_array( $term ) && ! empty( $term ) ) { - $term = reset( $term ); - return new Follower( $term->name ); - } - - return null; + $posts = self::get_followers( $user_id, null, null, array( 'name' => $actor ) ); + return is_empty( $posts ) ? null : $posts[0]; } /** @@ -321,21 +299,20 @@ class Followers { */ public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { $defaults = array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, - 'object_ids' => $user_id, + 'post_type' => self::POST_TYPE, + 'author' => $user_id, 'number' => $number, 'offset' => $offset, 'orderby' => 'id', - 'order' => 'ASC', + 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); - $terms = new WP_Term_Query( $args ); + $query = new WP_Query( $args ); $items = array(); - foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + foreach ( $query->get_posts() as $post ) { + $items[] = new Follower( $post ); // phpcs:ignore } return $items; @@ -348,16 +325,12 @@ class Followers { * * @return array The Term list of Followers. */ - public static function get_all_followers( $args = array() ) { - $defaults = array( - 'taxonomy' => self::TAXONOMY, - 'hide_empty' => false, + public static function get_all_followers( $user_id = null ) { + $args = array( + 'author' => null, + 'nopaging' => true, ); - - $args = wp_parse_args( $args, $defaults ); - $terms = new WP_Term_Query( $args ); - - return $terms->get_terms(); + return self::get_followers( $user_id, null, null, $args ); } /** @@ -367,8 +340,10 @@ class Followers { * * @return int The number of Followers */ - public static function count_followers( $user_id ) { - return count( self::get_followers( $user_id ) ); + public static function count_followers( $user_id = null ) { + // todo: rethink this. Don't we already get a total_posts count out of WP_Query? + // in the absence of that: caching. + return count( self::get_all_followers( $user_id ) ); } /** @@ -389,7 +364,7 @@ class Followers { // get all Followers of a ID of the WordPress User $terms = new WP_Term_Query( array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'hide_empty' => false, 'object_ids' => $user_id, 'fields' => 'ids', @@ -436,7 +411,7 @@ class Followers { */ public static function get_outdated_followers( $number = 50, $older_than = 604800 ) { $args = array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'number' => $number, 'meta_key' => 'updated_at', 'orderby' => 'meta_value_num', @@ -455,7 +430,7 @@ class Followers { $items = array(); foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + $items[] = new Follower( $follower ); // phpcs:ignore } return $items; @@ -471,7 +446,7 @@ class Followers { */ public static function get_faulty_followers( $number = 10 ) { $args = array( - 'taxonomy' => self::TAXONOMY, + 'taxonomy' => self::POST_TYPE, 'number' => $number, 'meta_query' => array( array( @@ -485,7 +460,7 @@ class Followers { $items = array(); foreach ( $terms->get_terms() as $follower ) { - $items[] = new Follower( $follower->name ); // phpcs:ignore + $items[] = new Follower( $follower ); // phpcs:ignore } return $items; diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index a663270..43a5934 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -9,6 +9,7 @@ use Activitypub\Collection\Followers; * This Object represents a single Follower. * There is no direct reference to a WordPress User here. * + * @author Matt Wiebe * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox @@ -61,6 +62,11 @@ class Follower { */ private $avatar; + /** + * The URL to the Follower + */ + private $url; + /** * The URL to the Followers Inbox * @@ -108,6 +114,12 @@ class Follower { */ private $errors; + /** + * The WordPress User ID, or 0 for whole site. + * @var int + */ + private $user_id; + /** * Maps the meta fields to the local db fields * @@ -117,22 +129,29 @@ class Follower { 'name' => 'name', 'preferredUsername' => 'username', 'inbox' => 'inbox', + 'url' => 'url', ); /** * Constructor * - * @param WP_Post $post + * @param string|WP_Post $actor The Actor-URL or WP_Post Object. + * @param int $user_id The WordPress User ID. 0 Represents the whole site. */ - public function __construct( $actor ) { - $this->actor = $actor; + public function __construct( $actor, $user_id = 0 ) { + $this->user_id = $user_id; + if ( is_a( $actor, 'WP_Post' ) ) { + $post = $actor; + $this->actor = $post->post_name; + $this->user_id = $post->post_author; + } else { + $this->actor = $actor; + $post = Followers::get_follower( $user_id, $actor ); + } - $term = get_term_by( 'name', $actor, Followers::TAXONOMY ); - - if ( $term ) { - $this->id = $term->term_id; - $this->slug = $term->slug; - $this->meta = json_decode( $term->meta ); + if ( $post ) { + $this->id = $term->post_id; + $this->slug = $term->post_name; } } @@ -205,25 +224,38 @@ class Follower { * @return mixed The attribute value. */ public function get( $attribute ) { - if ( $this->$attribute ) { + if ( ! is_null( $this->$attribute ) ) { return $this->$attribute; } - - $attribute = get_term_meta( $this->id, $attribute, true ); - if ( $attribute ) { - $this->$attribute = $attribute; - return $attribute; + $attribute_value = get_post_meta( $this->id, $attribute, true ); + if ( $attribute_value ) { + $this->$attribute = $attribute_value; + return $attribute_value; } - $attribute = $this->get_meta_by( $attribute ); - if ( $attribute ) { - $this->$attribute = $attribute; - return $attribute; + $attribute_value = $this->get_meta_by( $attribute ); + if ( $attribute_value ) { + $this->$attribute = $attribute_value; + return $attribute_value; } return null; } + /** + * Get a URL for the follower. Creates one out of the actor if no URL was set. + */ + public function get_url() { + if ( $this->get( 'url' ) ) { + return $this->get( 'url' ); + } + $actor = $this->get_actor(); + // normalize + $actor = ltrim( $actor, '@' ); + $parts = explode( '@', $actor ); + return sprintf( 'https://%s/@%s', $parts[1], $parts[0] ); + } + /** * Set new Error * @@ -246,7 +278,7 @@ class Follower { return $this->errors; } - $this->errors = get_term_meta( $this->id, 'errors' ); + $this->errors = get_post_meta( $this->id, 'errors' ); return $this->errors; } @@ -298,7 +330,9 @@ class Follower { */ public function get_meta_by( $attribute ) { $meta = $this->get_meta(); - + if ( ! is_array( $meta ) ) { + return null; + } // try mapped data (see $this->map_meta) foreach ( $this->map_meta as $remote => $local ) { if ( $attribute === $local && isset( $meta[ $remote ] ) ) { @@ -306,11 +340,6 @@ class Follower { } } - // try ActivityPub attribtes - if ( ! empty( $this->map_meta[ $attribute ] ) ) { - return $this->map_meta[ $attribute ]; - } - return null; } @@ -333,16 +362,8 @@ class Follower { * @return void */ public function update() { - $term = wp_update_term( - $this->id, - Followers::TAXONOMY, - array( - 'description' => wp_json_encode( $this->get_meta( true ) ), - ) - ); - $this->updated_at = \time(); - $this->update_term_meta(); + $this->save( $this->id ); } /** @@ -350,19 +371,16 @@ class Follower { * * @return void */ - public function save() { - $term = wp_insert_term( - $this->actor, - Followers::TAXONOMY, - array( - 'slug' => sanitize_title( $this->get_actor() ), - 'description' => wp_json_encode( $this->get_meta() ), - ) + public function save( $post_id = null ) { + $args = array( + 'ID' => $post_id, + 'post_name' => $this->actor, + 'post_author' => $this->user_id, + 'post_type' => Followers::POST_TYPE, + 'meta_input' => $this->get_post_meta_input(), ); - - $this->id = $term['term_id']; - - $this->update_term_meta(); + $post = wp_insert_post( $args ); + $this->id = $post->ID; } /** @@ -384,7 +402,7 @@ class Follower { * @return void */ public function delete() { - wp_delete_term( $this->id, Followers::TAXONOMY ); + wp_delete_post( $this->id ); } /** @@ -392,12 +410,14 @@ class Follower { * * @return void */ - protected function update_term_meta() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); + protected function get_post_meta_input() { + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username', 'url' ); + + $meta_input = array(); foreach ( $attributes as $attribute ) { if ( $this->get( $attribute ) ) { - update_term_meta( $this->id, $attribute, $this->get( $attribute ) ); + $meta_input[ $attribute ] = $this->get( $attribute ); } } @@ -410,7 +430,9 @@ class Follower { $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); } - add_term_meta( $this->id, 'errors', $error ); + $meta_input['errors'] = array( $error ); } + + return $meta_input; } } From 133de30b68fae8dc1b349640423cad714018bd7e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:44:50 +0200 Subject: [PATCH 230/427] remove wp-sweep filter, because post-types are not a problem --- activitypub.php | 11 -------- integration/class-wp-sweep.php | 50 ---------------------------------- 2 files changed, 61 deletions(-) delete mode 100644 integration/class-wp-sweep.php diff --git a/activitypub.php b/activitypub.php index 8ca78f8..8f665fc 100644 --- a/activitypub.php +++ b/activitypub.php @@ -151,17 +151,6 @@ add_action( 0 ); -add_action( - 'plugins_loaded', - function() { - if ( defined( 'WP_SWEEP_VERSION' ) ) { - require_once \dirname( __FILE__ ) . '/integration/class-wp-sweep.php'; - Integration\Wp_Sweep::init(); - } - }, - 0 -); - /** * `get_plugin_data` wrapper * diff --git a/integration/class-wp-sweep.php b/integration/class-wp-sweep.php deleted file mode 100644 index 730edba..0000000 --- a/integration/class-wp-sweep.php +++ /dev/null @@ -1,50 +0,0 @@ - 'ids' ) ); - - $excluded_term_ids = array_merge( $excluded_term_ids, $followers ); - - return array_unique( $excluded_term_ids ); - } -} From 7ed998d81f8d9a70a4802057c803ab9956b382e8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:45:25 +0200 Subject: [PATCH 231/427] fix follower table --- includes/table/class-followers.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 03fb70f..b83403f 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -16,6 +16,7 @@ class Followers extends WP_List_Table { 'name' => \__( 'Name', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), 'identifier' => \__( 'Identifier', 'activitypub' ), + 'updated_at' => \__( 'Last updated', 'activitypub' ), 'errors' => \__( 'Errors', 'activitypub' ), 'latest-error' => \__( 'Latest Error Message', 'activitypub' ), ); @@ -35,8 +36,8 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); - $counter = FollowerCollection::count_followers( \get_current_user_id() ); + $followers = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( \get_current_user_id() ); $this->items = array(); $this->set_pagination_args( @@ -47,12 +48,13 @@ class Followers extends WP_List_Table { ) ); - foreach ( $follower as $follower ) { + foreach ( $followers as $follower ) { $item = array( 'avatar' => esc_attr( $follower->get_avatar() ), 'name' => esc_attr( $follower->get_name() ), 'username' => esc_attr( $follower->get_username() ), 'identifier' => esc_attr( $follower->get_actor() ), + 'updated_at' => esc_attr( $follower->get_updated_at() ), 'errors' => $follower->count_errors(), 'latest-error' => $follower->get_latest_error_message(), ); From 8b7744a5eaf4f62f7dfced5af277c48384da95f7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:47:50 +0200 Subject: [PATCH 232/427] fix queries --- includes/collection/class-followers.php | 77 +++++++++++++++---------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 7217666..940e9f2 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -3,7 +3,7 @@ namespace Activitypub\Collection; use WP_Error; use Exception; -use WP_Term_Query; +use WP_Query; use Activitypub\Http; use Activitypub\Webfinger; use Activitypub\Model\Activity; @@ -19,7 +19,7 @@ use function Activitypub\get_remote_metadata_by_actor; * @author Matthias Pfefferle */ class Followers { - const POST_TYPE = 'activitypub_followers'; + const POST_TYPE = 'activitypub_follower'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; /** @@ -50,7 +50,7 @@ class Followers { 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), ), - 'public' => false, + 'public' => true, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, @@ -213,16 +213,15 @@ class Followers { return $meta; } - $follower = new Follower( $actor, $user_id ); + $follower = new Follower( $actor ); $follower->from_meta( $meta ); $follower->upsert(); - if ( is_wp_error( $result ) ) { - return $result; - } else { - wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); - return $follower; - } + add_post_meta( $follower->get_id(), 'user_id', $user_id ); + + wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + + return $follower; } /** @@ -235,20 +234,34 @@ class Followers { */ public static function remove_follower( $user_id, $actor ) { wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + return wp_remove_object_terms( $user_id, $actor, self::POST_TYPE ); } /** - * Remove a Follower + * Get a Follower * * @param int $user_id The ID of the WordPress User * @param string $actor The Actor URL * * @return \Activitypub\Model\Follower The Follower object */ - public static function get_follower( $user_id, $actor ) { - $posts = self::get_followers( $user_id, null, null, array( 'name' => $actor ) ); - return is_empty( $posts ) ? null : $posts[0]; + public static function get_follower( $actor ) { + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $actor ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + return new Follower( $post ); + } + + return null; } /** @@ -299,12 +312,17 @@ class Followers { */ public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { $defaults = array( - 'post_type' => self::POST_TYPE, - 'author' => $user_id, - 'number' => $number, - 'offset' => $offset, - 'orderby' => 'id', - 'order' => 'DESC', + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + 'offset' => $offset, + 'orderby' => 'ID', + 'order' => 'DESC', + 'meta_query' => array( + array( + 'key' => 'user_id', + 'value' => $user_id, + ), + ), ); $args = wp_parse_args( $args, $defaults ); @@ -362,11 +380,10 @@ class Followers { } // get all Followers of a ID of the WordPress User - $terms = new WP_Term_Query( + $terms = new WP_Query( array( - 'taxonomy' => self::POST_TYPE, - 'hide_empty' => false, - 'object_ids' => $user_id, + 'post_type' => self::POST_TYPE, + 'author' => $user_id, 'fields' => 'ids', 'meta_query' => array( array( @@ -377,16 +394,16 @@ class Followers { ) ); - $terms = $terms->get_terms(); + $posts = $posts->get_posts(); - if ( ! $terms ) { + if ( ! $posts ) { return array(); } global $wpdb; $results = $wpdb->get_col( $wpdb->prepare( - "SELECT DISTINCT meta_value FROM {$wpdb->termmeta} + "SELECT DISTINCT meta_value FROM {$wpdb->posts} WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ") AND meta_key = 'shared_inbox' AND meta_value IS NOT NULL", @@ -446,9 +463,9 @@ class Followers { */ public static function get_faulty_followers( $number = 10 ) { $args = array( - 'taxonomy' => self::POST_TYPE, - 'number' => $number, - 'meta_query' => array( + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + 'meta_query' => array( array( 'key' => 'errors', 'compare' => 'EXISTS', From fc0fc295bb6d639e503815625b89f2719becc0ec Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:48:09 +0200 Subject: [PATCH 233/427] fix follower creation --- includes/model/class-follower.php | 70 +++++++++++++++++-------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 43a5934..9e85cd6 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -1,6 +1,7 @@ user_id = $user_id; - if ( is_a( $actor, 'WP_Post' ) ) { + public function __construct( $actor ) { + $post = null; + + if ( \is_a( $actor, 'WP_Post' ) ) { $post = $actor; - $this->actor = $post->post_name; - $this->user_id = $post->post_author; } else { - $this->actor = $actor; - $post = Followers::get_follower( $user_id, $actor ); + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $actor ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + } else { + $this->actor = $actor; + } } if ( $post ) { - $this->id = $term->post_id; - $this->slug = $term->post_name; + $this->id = $post->ID; + $this->actor = $post->guid; + $this->updated_at = $post->post_modified; } } @@ -227,7 +230,9 @@ class Follower { if ( ! is_null( $this->$attribute ) ) { return $this->$attribute; } + $attribute_value = get_post_meta( $this->id, $attribute, true ); + if ( $attribute_value ) { $this->$attribute = $attribute_value; return $attribute_value; @@ -288,7 +293,7 @@ class Follower { * @return void */ public function reset_errors() { - delete_term_meta( $this->id, 'errors' ); + delete_post_meta( $this->id, 'errors' ); } /** @@ -363,7 +368,7 @@ class Follower { */ public function update() { $this->updated_at = \time(); - $this->save( $this->id ); + $this->save(); } /** @@ -371,16 +376,19 @@ class Follower { * * @return void */ - public function save( $post_id = null ) { + public function save() { $args = array( - 'ID' => $post_id, - 'post_name' => $this->actor, - 'post_author' => $this->user_id, - 'post_type' => Followers::POST_TYPE, - 'meta_input' => $this->get_post_meta_input(), + 'ID' => $this->id, + 'guid' => $this->actor, + 'post_title' => $this->get_name(), + 'post_author' => 0, + 'post_type' => Followers::POST_TYPE, + 'post_content' => wp_json_encode( $this->meta ), + 'post_modified' => gmdate( 'Y-m-d H:i:s', $this->updated_at ), + 'meta_input' => $this->get_post_meta_input(), ); - $post = wp_insert_post( $args ); - $this->id = $post->ID; + $post_id = wp_insert_post( $args ); + $this->id = $post_id; } /** @@ -406,12 +414,12 @@ class Follower { } /** - * Update the term meta. + * Update the post meta. * * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username', 'url' ); + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username' ); $meta_input = array(); From 9036b644d1ad2521a0630de080069228fdec4ab6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:48:43 +0200 Subject: [PATCH 234/427] add user connection --- includes/class-migration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index 7e7cd9a..36882cb 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -83,7 +83,7 @@ class Migration { $follower->upsert(); - $result = wp_set_object_terms( $user_id, $follower->get_actor(), Followers::TAXONOMY, true ); + add_post_meta( $follower->get_id(), 'user_id', $user_id ); } } } From fcf6740d36272cfee4dea01c4fe1e753b088bab0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 11:53:07 +0200 Subject: [PATCH 235/427] fix query --- includes/collection/class-followers.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 940e9f2..f652d24 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -380,16 +380,19 @@ class Followers { } // get all Followers of a ID of the WordPress User - $terms = new WP_Query( + $posts = new WP_Query( array( 'post_type' => self::POST_TYPE, - 'author' => $user_id, 'fields' => 'ids', 'meta_query' => array( array( 'key' => 'inbox', 'compare' => 'EXISTS', ), + array( + 'key' => 'user_id', + 'value' => $user_id, + ), ), ) ); From 441412150278170bcaa8b1f836b326e0fbf0904c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 12:13:30 +0200 Subject: [PATCH 236/427] add missing user_id --- includes/collection/class-followers.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index f652d24..96d268b 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -241,12 +241,12 @@ class Followers { /** * Get a Follower * - * @param int $user_id The ID of the WordPress User - * @param string $actor The Actor URL + * @param int $user_id The ID of the WordPress User + * @param string $actor The Actor URL * * @return \Activitypub\Model\Follower The Follower object */ - public static function get_follower( $actor ) { + public static function get_follower( $user_id, $actor ) { global $wpdb; $post_id = $wpdb->get_var( From 37c61fbf07b7bd9dc50aae2b0f6e804ba29f83cd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 12:17:48 +0200 Subject: [PATCH 237/427] fix queries --- includes/collection/class-followers.php | 29 +++++++++++-------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 96d268b..601d129 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -431,25 +431,22 @@ class Followers { */ public static function get_outdated_followers( $number = 50, $older_than = 604800 ) { $args = array( - 'taxonomy' => self::POST_TYPE, - 'number' => $number, - 'meta_key' => 'updated_at', - 'orderby' => 'meta_value_num', - 'order' => 'DESC', - 'meta_query' => array( + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + 'orderby' => 'modified', + 'order' => 'DESC', + 'date_query' => array( array( - 'key' => 'updated_at', - 'value' => time() - $older_than, - 'type' => 'numeric', - 'compare' => '<=', + 'column' => 'post_modified_gmt', + 'before' => 604800, ), ), ); - $terms = new WP_Term_Query( $args ); + $posts = new WP_Query( $args ); $items = array(); - foreach ( $terms->get_terms() as $follower ) { + foreach ( $posts->get_posts() as $follower ) { $items[] = new Follower( $follower ); // phpcs:ignore } @@ -470,16 +467,16 @@ class Followers { 'posts_per_page' => $number, 'meta_query' => array( array( - 'key' => 'errors', - 'compare' => 'EXISTS', + 'key' => 'errors', + 'compare' => 'EXISTS', ), ), ); - $terms = new WP_Term_Query( $args ); + $posts = new WP_Query( $args ); $items = array(); - foreach ( $terms->get_terms() as $follower ) { + foreach ( $posts->get_posts() as $follower ) { $items[] = new Follower( $follower ); // phpcs:ignore } From 46f376e05eb7d52b4a2757f17a0be23f99769bf5 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 15 Jun 2023 12:24:13 +0200 Subject: [PATCH 238/427] fix tests --- includes/model/class-follower.php | 5 ++++- tests/test-class-db-activitypub-followers.php | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 9e85cd6..7b41bb0 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -367,7 +367,10 @@ class Follower { * @return void */ public function update() { - $this->updated_at = \time(); + if ( ! $this->updated_at ) { + $this->updated_at = \time(); + } + $this->save(); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index a04145a..e660c00 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -109,7 +109,8 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); - update_term_meta( $follower->get_id(), 'updated_at', \time() - 804800 ); + $follower->set_updates_at( \time() - 804800 ); + $follower->update(); $followers = \Activitypub\Collection\Followers::get_outdated_followers(); $this->assertEquals( 1, count( $followers ) ); @@ -129,7 +130,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); for ( $i = 1; $i <= 15; $i++ ) { - add_term_meta( $follower->get_id(), 'errors', 'error ' . $i ); + add_post_meta( $follower->get_id(), 'errors', 'error ' . $i ); } $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); From 793214cea2a438805db0e28653d8b2c0c5eff131 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 16 Jun 2023 11:40:26 +0200 Subject: [PATCH 239/427] now tests are green again --- docker-compose-test.yml | 8 ++++++ includes/collection/class-followers.php | 22 ++++++++-------- includes/model/class-follower.php | 6 ++--- tests/test-class-db-activitypub-followers.php | 26 ++++++++++++++++--- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/docker-compose-test.yml b/docker-compose-test.yml index 4c69e44..411f3fd 100644 --- a/docker-compose-test.yml +++ b/docker-compose-test.yml @@ -6,11 +6,19 @@ services: environment: MYSQL_DATABASE: activitypub-test MYSQL_ROOT_PASSWORD: activitypub-test + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3306"] + interval: 5s + timeout: 2s + retries: 5 test-php: build: context: . dockerfile: Dockerfile + depends_on: + test-db: + condition: service_healthy links: - test-db volumes: diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 601d129..ab5be32 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -217,7 +217,7 @@ class Followers { $follower->from_meta( $meta ); $follower->upsert(); - add_post_meta( $follower->get_id(), 'user_id', $user_id ); + update_post_meta( $follower->get_id(), 'user_id', $user_id, $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); @@ -339,14 +339,13 @@ class Followers { /** * Get all Followers * - * @param array $args The WP_Term_Query arguments. + * @param array $args The WP_Query arguments. * * @return array The Term list of Followers. */ public static function get_all_followers( $user_id = null ) { $args = array( - 'author' => null, - 'nopaging' => true, + 'meta_query' => array(), ); return self::get_followers( $user_id, null, null, $args ); } @@ -358,10 +357,10 @@ class Followers { * * @return int The number of Followers */ - public static function count_followers( $user_id = null ) { + public static function count_followers( $user_id ) { // todo: rethink this. Don't we already get a total_posts count out of WP_Query? // in the absence of that: caching. - return count( self::get_all_followers( $user_id ) ); + return count( self::get_followers( $user_id ) ); } /** @@ -406,11 +405,11 @@ class Followers { global $wpdb; $results = $wpdb->get_col( $wpdb->prepare( - "SELECT DISTINCT meta_value FROM {$wpdb->posts} - WHERE term_id IN (" . implode( ', ', array_fill( 0, count( $terms ), '%d' ) ) . ") + "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} + WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") AND meta_key = 'shared_inbox' AND meta_value IS NOT NULL", - $terms + $posts ) ); @@ -435,10 +434,11 @@ class Followers { 'posts_per_page' => $number, 'orderby' => 'modified', 'order' => 'DESC', - 'date_query' => array( + 'post_status' => 'any', // 'any' includes 'trash + 'date_query' => array( array( 'column' => 'post_modified_gmt', - 'before' => 604800, + 'before' => gmdate( 'Y-m-d', \time() - $older_than ), ), ), ); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 7b41bb0..0a6a6ef 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -367,10 +367,7 @@ class Follower { * @return void */ public function update() { - if ( ! $this->updated_at ) { - $this->updated_at = \time(); - } - + $this->updated_at = \time(); $this->save(); } @@ -387,6 +384,7 @@ class Follower { 'post_author' => 0, 'post_type' => Followers::POST_TYPE, 'post_content' => wp_json_encode( $this->meta ), + 'post_status' => 'publish', 'post_modified' => gmdate( 'Y-m-d H:i:s', $this->updated_at ), 'meta_input' => $this->get_post_meta_input(), ); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index e660c00..87e0064 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -65,7 +65,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $db_followers ); - $this->assertSame( array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ), $db_followers ); + $this->assertEquals( array( 'http://sally.example.org', 'https://example.org/author/doe', 'https://example.com/author/jon' ), $db_followers ); } public function test_add_follower() { @@ -109,8 +109,28 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); - $follower->set_updates_at( \time() - 804800 ); - $follower->update(); + global $wpdb; + + //eg. time one year ago.. + $time = time() - 804800; + $mysql_time_format = 'Y-m-d H:i:s'; + + $post_modified = gmdate( $mysql_time_format, $time ); + $post_modified_gmt = gmdate( $mysql_time_format, ( $time + get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ); + $post_id = $follower->get_id(); + + $wpdb->query( + $wpdb->prepare( + "UPDATE $wpdb->posts SET post_modified = %s, post_modified_gmt = %s WHERE ID = %s", + array( + $post_modified, + $post_modified_gmt, + $post_id, + ) + ) + ); + + clean_post_cache( $post_id ); $followers = \Activitypub\Collection\Followers::get_outdated_followers(); $this->assertEquals( 1, count( $followers ) ); From daf7acb1b047bd1898d55c506617ca16d34eb7de Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 16 Jun 2023 16:46:49 +0200 Subject: [PATCH 240/427] implement missing get_follower logic --- includes/collection/class-followers.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index ab5be32..16a55a2 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -235,7 +235,13 @@ class Followers { public static function remove_follower( $user_id, $actor ) { wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); - return wp_remove_object_terms( $user_id, $actor, self::POST_TYPE ); + $follower = self::get_follower( $user_id, $actor ); + + if ( ! $follower ) { + return false; + } + + return delete_post_meta( $follower->get_id(), 'user_id', $user_id ); } /** @@ -251,8 +257,12 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $actor ) + "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'user_id' AND pm.meta_value = %d AND p.guid = %s", + array( + esc_sql( self::POST_TYPE ), + esc_sql( $user_id ), + esc_sql( $actor ), + ) ) ); From 28922d51ddbb96cb13506c025e8cf16187621f3a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 16 Jun 2023 16:56:30 +0200 Subject: [PATCH 241/427] Fix follower list --- includes/collection/class-followers.php | 17 ++++++++++++++--- includes/table/class-followers.php | 7 ++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 16a55a2..e73ee1b 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -368,9 +368,20 @@ class Followers { * @return int The number of Followers */ public static function count_followers( $user_id ) { - // todo: rethink this. Don't we already get a total_posts count out of WP_Query? - // in the absence of that: caching. - return count( self::get_followers( $user_id ) ); + $query = new WP_Query( + array( + 'post_type' => self::POST_TYPE, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'user_id', + 'value' => $user_id, + ), + ), + ) + ); + + return $query->found_posts; } /** diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index b83403f..2bece58 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -112,7 +112,12 @@ class Followers extends WP_List_Table { switch ( $this->current_action() ) { case 'delete': - FollowerCollection::remove_follower( \get_current_user_id(), $followers ); + if ( ! is_array( $followers ) ) { + $followers = array( $followers ); + } + foreach ( $followers as $follower ) { + FollowerCollection::remove_follower( \get_current_user_id(), $follower ); + } break; } } From 08e3104a1ee24f5d9239364516242e6b913ade20 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 19 Jun 2023 11:04:45 +0200 Subject: [PATCH 242/427] better add_follower handling --- includes/collection/class-followers.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index e73ee1b..b7cbbd7 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -217,9 +217,12 @@ class Followers { $follower->from_meta( $meta ); $follower->upsert(); - update_post_meta( $follower->get_id(), 'user_id', $user_id, $user_id ); + $meta = get_post_meta( $follower->get_id(), 'user_id' ); - wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { + add_post_meta( $follower->get_id(), 'user_id', $user_id ); + wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); + } return $follower; } From a71f79e979dc8303367e8c26632ecf0f05947ea4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 19 Jun 2023 11:05:01 +0200 Subject: [PATCH 243/427] test remove_follower --- tests/test-class-db-activitypub-followers.php | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 87e0064..a803c03 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -31,6 +31,12 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { 'name' => '12345', 'prefferedUsername' => '12345', ), + 'user2@example.com' => array( + 'url' => 'https://user2.example.com', + 'inbox' => 'https://user2.example.com/inbox', + 'name' => 'user2', + 'prefferedUsername' => 'user2', + ), ); public function set_up() { @@ -73,15 +79,21 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); $follower = 'https://12345.example.com'; + $follower2 = 'https://user2.example.com'; \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 2, $follower ); + \Activitypub\Collection\Followers::add_follower( 2, $follower2 ); $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); + $db_followers2 = \Activitypub\Collection\Followers::get_followers( 2 ); $this->assertContains( $follower, $db_followers ); + $this->assertContains( $follower2, $db_followers2 ); } public function test_get_follower() { $followers = array( 'https://example.com/author/jon' ); + $followers2 = array( 'https://user2.example.com' ); $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); @@ -90,11 +102,57 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Collection\Followers::add_follower( 1, $follower ); } + foreach ( $followers2 as $follower ) { + \Activitypub\Collection\Followers::add_follower( 2, $follower ); + } + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $this->assertNull( $follower ); + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://user2.example.com' ); + $this->assertNull( $follower ); + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + + $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://user2.example.com' ); + $this->assertEquals( 'https://user2.example.com', $follower2->get_actor() ); + } + + public function test_delete_follower() { + $followers = array( 'https://example.com/author/jon' ); + $followers2 = array( 'https://user2.example.com' ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 2, $follower ); + } + + foreach ( $followers2 as $follower2 ) { + \Activitypub\Collection\Followers::add_follower( 2, $follower2 ); + } + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + + $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + + \Activitypub\Collection\Followers::remove_follower( 1, 'https://example.com/author/jon' ); + + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); + $this->assertNull( $follower ); + + $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); } public function test_get_outdated_followers() { From bbf40a5fec7968d79f67a6b3bae48819e5d55581 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 19 Jun 2023 11:10:15 +0200 Subject: [PATCH 244/427] added more tests --- tests/test-class-db-activitypub-followers.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index a803c03..482cecc 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -123,7 +123,10 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } public function test_delete_follower() { - $followers = array( 'https://example.com/author/jon' ); + $followers = array( + 'https://example.com/author/jon', + 'https://example.org/author/doe', + ); $followers2 = array( 'https://user2.example.com' ); $pre_http_request = new MockAction(); @@ -143,6 +146,9 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $followers = \Activitypub\Collection\Followers::get_followers( 1 ); + $this->assertEquals( 2, count( $followers ) ); + $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); @@ -153,6 +159,9 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + + $followers = \Activitypub\Collection\Followers::get_followers( 1 ); + $this->assertEquals( 1, count( $followers ) ); } public function test_get_outdated_followers() { From e7bc9706a8d3dbe39ac99a8caaac368ce77b359f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 19 Jun 2023 11:36:59 +0200 Subject: [PATCH 245/427] remove url attribute --- includes/collection/class-followers.php | 2 +- includes/model/class-follower.php | 20 -------------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index b7cbbd7..49569e5 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -50,7 +50,7 @@ class Followers { 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), ), - 'public' => true, + 'public' => false, 'hierarchical' => false, 'rewrite' => false, 'query_var' => false, diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 0a6a6ef..660bec5 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -53,11 +53,6 @@ class Follower { */ private $avatar; - /** - * The URL to the Follower - */ - private $url; - /** * The URL to the Followers Inbox * @@ -120,7 +115,6 @@ class Follower { 'name' => 'name', 'preferredUsername' => 'username', 'inbox' => 'inbox', - 'url' => 'url', ); /** @@ -247,20 +241,6 @@ class Follower { return null; } - /** - * Get a URL for the follower. Creates one out of the actor if no URL was set. - */ - public function get_url() { - if ( $this->get( 'url' ) ) { - return $this->get( 'url' ); - } - $actor = $this->get_actor(); - // normalize - $actor = ltrim( $actor, '@' ); - $parts = explode( '@', $actor ); - return sprintf( 'https://%s/@%s', $parts[1], $parts[0] ); - } - /** * Set new Error * From a2152037778815d8d64fbeeb470df936a90adc82 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 20 Jun 2023 09:51:13 +0200 Subject: [PATCH 246/427] because post_types have length limitations, we should abbreviate the "activitypub" prefix, to be more flexible and consistent when adding other post_types in the future "Must not exceed 20 characters and may only contain lowercase alphanumeric characters, dashes, and underscores" --- includes/collection/class-followers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 49569e5..95f7aa8 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -19,7 +19,7 @@ use function Activitypub\get_remote_metadata_by_actor; * @author Matthias Pfefferle */ class Followers { - const POST_TYPE = 'activitypub_follower'; + const POST_TYPE = 'ap_follower'; const CACHE_KEY_INBOXES = 'follower_inboxes_%s'; /** From d47a048329613e2f20e8dc8e09dfe6639dc417cd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 22 Jun 2023 10:01:15 +0200 Subject: [PATCH 247/427] save `meta` to post-meta and persist summary in post-content --- includes/model/class-follower.php | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 660bec5..5174f36 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -46,6 +46,13 @@ class Follower { */ private $username; + /** + * The User-Summary + * + * @var string + */ + private $summary; + /** * The Avatar URL * @@ -328,19 +335,6 @@ class Follower { return null; } - /** - * Get the meta data. - * - * @return array $meta The meta data. - */ - public function get_meta() { - if ( $this->meta ) { - return $this->meta; - } - - return null; - } - /** * Update the current Follower-Object. * @@ -363,7 +357,7 @@ class Follower { 'post_title' => $this->get_name(), 'post_author' => 0, 'post_type' => Followers::POST_TYPE, - 'post_content' => wp_json_encode( $this->meta ), + 'post_content' => $this->get_summary(), 'post_status' => 'publish', 'post_modified' => gmdate( 'Y-m-d H:i:s', $this->updated_at ), 'meta_input' => $this->get_post_meta_input(), @@ -400,7 +394,7 @@ class Follower { * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username' ); + $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username', 'meta' ); $meta_input = array(); From 5478be13555611826624db7c8164e4091edc45fa Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 23 Jun 2023 14:54:29 +0200 Subject: [PATCH 248/427] a follower is now a valid ActivityPub Actor this helps with API handling --- includes/activity/class-activity-object.php | 562 ++++++++++++++++++ includes/activity/class-actor.php | 117 ++++ includes/activity/class-person.php | 20 + includes/class-migration.php | 15 +- includes/class-scheduler.php | 4 +- includes/collection/class-followers.php | 71 ++- includes/functions.php | 11 + includes/model/class-follower.php | 370 ++++-------- includes/rest/class-followers.php | 2 +- includes/table/class-followers.php | 16 +- templates/followers-list.php | 8 +- ...-class-activitypub-activity-dispatcher.php | 3 + tests/test-class-activitypub-mention.php | 1 + tests/test-class-db-activitypub-followers.php | 32 +- 14 files changed, 914 insertions(+), 318 deletions(-) create mode 100644 includes/activity/class-activity-object.php create mode 100644 includes/activity/class-actor.php create mode 100644 includes/activity/class-person.php diff --git a/includes/activity/class-activity-object.php b/includes/activity/class-activity-object.php new file mode 100644 index 0000000..0115a40 --- /dev/null +++ b/includes/activity/class-activity-object.php @@ -0,0 +1,562 @@ + + * | array + * | null + */ + protected $attachment; + + /** + * One or more entities to which this object is attributed. + * The attributed entities might not be Actors. For instance, an + * object might be attributed to the completion of another activity. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $attributed_to; + + /** + * One or more entities that represent the total population of + * entities for which the object can considered to be relevant. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $audience; + + /** + * The content or textual representation of the Object encoded as a + * JSON string. By default, the value of content is HTML. + * The mediaType property can be used in the object to indicate a + * different content type. + * + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content + * + * @var string|null + */ + protected $content; + + /** + * The context within which the object exists or an activity was + * performed. + * The notion of "context" used is intentionally vague. + * The intended function is to serve as a means of grouping objects + * and activities that share a common originating context or + * purpose. An example could be all activities relating to a common + * project or event. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $context; + + /** + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content + * + * @var array|null + */ + protected $content_map; + + /** + * A simple, human-readable, plain-text name for the object. + * HTML markup MUST NOT be included. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name + * + * @var string|null xsd:string + */ + protected $name; + + /** + * The name MAY be expressed using multiple language-tagged values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name + * + * @var array|null rdf:langString + */ + protected $name_map; + + /** + * The date and time describing the actual or expected ending time + * of the object. + * When used with an Activity object, for instance, the endTime + * property specifies the moment the activity concluded or + * is expected to conclude. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-endtime + * + * @var string|null + */ + protected $end_time; + + /** + * The entity (e.g. an application) that generated the object. + * + * @var string|null + */ + protected $generator; + + /** + * An entity that describes an icon for this object. + * The image should have an aspect ratio of one (horizontal) + * to one (vertical) and should be suitable for presentation + * at a small size. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon + * + * @var string + * | Image + * | Link + * | array + * | array + * | null + */ + protected $icon; + + /** + * An entity that describes an image for this object. + * Unlike the icon property, there are no aspect ratio + * or display size limitations assumed. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term + * + * @var string + * | Image + * | Link + * | array + * | array + * | null + */ + protected $image; + + /** + * One or more entities for which this object is considered a + * response. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $in_reply_to; + + /** + * One or more physical or logical locations associated with the + * object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $location; + + /** + * An entity that provides a preview of this object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $preview; + + /** + * The date and time at which the object was published + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published + * + * @var string|null xsd:dateTime + */ + protected $published; + + /** + * A Collection containing objects considered to be responses to + * this object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies + * + * @var string + * | Collection + * | Link + * | null + */ + protected $replies; + + /** + * The date and time describing the actual or expected starting time + * of the object. + * When used with an Activity object, for instance, the startTime + * property specifies the moment the activity began + * or is scheduled to begin. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-starttime + * + * @var string|null xsd:dateTime + */ + protected $start_time; + + /** + * A natural language summarization of the object encoded as HTML. + * Multiple language tagged summaries MAY be provided. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $summary; + + /** + * The content MAY be expressed using multiple language-tagged + * values. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary + * + * @var array|null + */ + protected $summary_map; + + /** + * One or more "tags" that have been associated with an objects. + * A tag can be any kind of Object. + * The key difference between attachment and tag is that the former + * implies association by inclusion, while the latter implies + * associated by reference. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $tag; + + /** + * The date and time at which the object was updated + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-updated + * + * @var string|null xsd:dateTime + */ + protected $updated; + + /** + * One or more links to representations of the object. + * + * @var string + * | array + * | Link + * | array + * | null + */ + protected $url; + + /** + * An entity considered to be part of the public primary audience + * of an Object + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $to; + + /** + * An Object that is part of the private primary audience of this + * Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $bto; + + /** + * An Object that is part of the public secondary audience of this + * Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $cc; + + /** + * One or more Objects that are part of the private secondary + * audience of this Object. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc + * + * @var string + * | ObjectType + * | Link + * | array + * | array + * | null + */ + protected $bcc; + + /** + * The MIME media type of the value of the content property. + * If not specified, the content property is assumed to contain + * text/html content. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype + * + * @var string|null + */ + protected $media_type; + + /** + * When the object describes a time-bound resource, such as an audio + * or video, a meeting, etc, the duration property indicates the + * object's approximate duration. + * The value MUST be expressed as an xsd:duration as defined by + * xmlschema11-2, section 3.3.6 (e.g. a period of 5 seconds is + * represented as "PT5S"). + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration + * + * @var string|null + */ + protected $duration; + + /** + * Intended to convey some sort of source from which the content + * markup was derived, as a form of provenance, or to support + * future editing by clients. + * + * @see https://www.w3.org/TR/activitypub/#source-property + * + * @var ObjectType + */ + protected $source; + + /** + * Magic function to implement getter and setter + * + * @param string $method The method name. + * @param string $params The method params. + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( $this->has( $var ) ) { + return $this->get( $var ); + } + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + $this->set( $var, $params[0] ); + } + + if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { + $this->add( $var, $params[0] ); + } + } + + /** + * Generic getter. + * + * @param string $key The key to get. + * + * @return mixed The value. + */ + public function get( $key ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + + } + + return $this->$key; + } + + /** + * Check if the object has a key + * + * @param string $key The key to check. + * + * @return boolean True if the object has the key. + */ + public function has( $key ) { + return property_exists( $this, $key ); + } + + /** + * Generic setter. + * + * @param string $key The key to set. + * @param string $value The value to set. + * + * @return mixed The value. + */ + public function set( $key, $value ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + } + + $this->$key = $value; + + return $this->$key; + } + + /** + * Generic adder. + * + * @param string $key The key to set. + * @param mixed $value The value to add. + * + * @return mixed The value. + */ + public function add( $key, $value ) { + if ( ! $this->has( $key ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); + } + + if ( ! isset( $this->$key ) ) { + $this->$key = array(); + } + + $this->$key[] = $value; + + return $this->$key; + } + + /** + * Convert JSON input to an array. + * + * @return string The JSON string. + * + * @return array An Object built from the JSON string. + */ + public static function from_json( $json ) { + $array = wp_json_decode( $json, true ); + + return self::from_array( $array ); + } + + /** + * Convert JSON input to an array. + * + * @return string The object array. + * + * @return array An Object built from the JSON string. + */ + public static function from_array( $array ) { + $object = new static(); + + foreach ( $array as $key => $value ) { + $key = camel_to_snake_case( $key ); + $object->set( $key, $value ); + } + + return $object; + } +} diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php new file mode 100644 index 0000000..e9f810b --- /dev/null +++ b/includes/activity/class-actor.php @@ -0,0 +1,117 @@ + 'https://my-example.com/actor#main-key' + * 'owner' => 'https://my-example.com/actor', + * 'publicKeyPem' => '-----BEGIN PUBLIC KEY----- + * MIIBI [...] + * DQIDAQAB + * -----END PUBLIC KEY-----' + * ] + * + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Signing_requests_using_HTTP_Signatures + * + * @var string|array|null + */ + protected $public_key; +} diff --git a/includes/activity/class-person.php b/includes/activity/class-person.php new file mode 100644 index 0000000..cf161bc --- /dev/null +++ b/includes/activity/class-person.php @@ -0,0 +1,20 @@ +set_error( $meta ); - } else { - $follower->from_meta( $meta ); } $follower->upsert(); - add_post_meta( $follower->get_id(), 'user_id', $user_id ); + add_post_meta( $follower->get__id(), '_user_id', $user_id ); } } } diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ea1a6e4..d55fb35 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -100,7 +100,7 @@ class Scheduler { $followers = Followers::get_outdated_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); + $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { $follower->set_error( $meta ); @@ -121,7 +121,7 @@ class Scheduler { $followers = Followers::get_faulty_followers(); foreach ( $followers as $follower ) { - $meta = get_remote_metadata_by_actor( $follower->get_actor(), true ); + $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( is_tombstone( $meta ) ) { $follower->delete(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 95f7aa8..96b9099 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -62,19 +62,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'name', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function( $value ) { - return sanitize_user( $value ); - }, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'username', + 'preferred_username', array( 'type' => 'string', 'single' => true, @@ -86,11 +74,11 @@ class Followers { register_post_meta( self::POST_TYPE, - 'avatar', + 'icon', array( - 'type' => 'string', + //'type' => 'string', 'single' => true, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), + //'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); @@ -116,7 +104,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'shared_inbox', + '_shared_inbox', array( 'type' => 'string', 'single' => true, @@ -126,7 +114,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'updated_at', + 'updated', array( 'type' => 'string', 'single' => true, @@ -142,7 +130,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'errors', + '_errors', array( 'type' => 'string', 'single' => false, @@ -156,6 +144,18 @@ class Followers { ) ); + register_post_meta( + self::POST_TYPE, + '_actor', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function( $value ) { + return esc_sql( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_post_type' ); } @@ -213,14 +213,13 @@ class Followers { return $meta; } - $follower = new Follower( $actor ); - $follower->from_meta( $meta ); + $follower = Follower::from_array( $meta ); $follower->upsert(); - $meta = get_post_meta( $follower->get_id(), 'user_id' ); + $meta = get_post_meta( $follower->get__id(), '_user_id' ); if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { - add_post_meta( $follower->get_id(), 'user_id', $user_id ); + add_post_meta( $follower->get__id(), '_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -244,7 +243,7 @@ class Followers { return false; } - return delete_post_meta( $follower->get_id(), 'user_id', $user_id ); + return delete_post_meta( $follower->get__id(), '_user_id', $user_id ); } /** @@ -260,7 +259,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -271,7 +270,7 @@ class Followers { if ( $post_id ) { $post = get_post( $post_id ); - return new Follower( $post ); + return Follower::from_custom_post_type( $post ); } return null; @@ -295,7 +294,7 @@ class Followers { } if ( isset( $object['user_id'] ) ) { - unset( $object['user_id'] ); + unset( $object['_user_id'] ); unset( $object['@context'] ); } @@ -304,7 +303,7 @@ class Followers { // send "Accept" activity $activity = new Activity( 'Accept' ); - $activity->set_object( $object ); + $activity->set_activity_object( $object ); $activity->set_actor( \get_author_posts_url( $user_id ) ); $activity->set_to( $actor ); $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); @@ -332,7 +331,7 @@ class Followers { 'order' => 'DESC', 'meta_query' => array( array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -343,7 +342,7 @@ class Followers { $items = array(); foreach ( $query->get_posts() as $post ) { - $items[] = new Follower( $post ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $post ); // phpcs:ignore } return $items; @@ -377,7 +376,7 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -409,11 +408,11 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => 'inbox', + 'key' => '_shared_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'user_id', + 'key' => '_user_id', 'value' => $user_id, ), ), @@ -431,7 +430,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = 'shared_inbox' + AND meta_key = '_shared_inbox' AND meta_value IS NOT NULL", $posts ) @@ -471,7 +470,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = new Follower( $follower ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore } return $items; @@ -501,7 +500,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = new Follower( $follower ); // phpcs:ignore + $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore } return $items; diff --git a/includes/functions.php b/includes/functions.php index 9837641..212060e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -237,6 +237,17 @@ function get_rest_url_by_path( $path = '' ) { return \get_rest_url( null, $namespaced_path ); } +/** + * Convert a string from camelCase to snake_case. + * + * @param string $string The string to convert. + * + * @return string The converted string. + */ +function camel_to_snake_case( $string ) { + return strtolower( preg_replace( '/(? 'name', - 'preferredUsername' => 'username', - 'inbox' => 'inbox', - ); - - /** - * Constructor - * - * @param string|WP_Post $actor The Actor-URL or WP_Post Object. - * @param int $user_id The WordPress User ID. 0 Represents the whole site. - */ - public function __construct( $actor ) { - $post = null; - - if ( \is_a( $actor, 'WP_Post' ) ) { - $post = $actor; - } else { - global $wpdb; - - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $actor ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - } else { - $this->actor = $actor; - } - } - - if ( $post ) { - $this->id = $post->ID; - $this->actor = $post->guid; - $this->updated_at = $post->post_modified; - } - } - - /** - * Magic function to implement getter and setter - * - * @param string $method The method name. - * @param string $params The method params. - * - * @return void - */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - if ( empty( $this->$var ) ) { - return $this->get( $var ); - } - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - } + protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * Magic function to return the Actor-URL when the Object is used as a string @@ -188,64 +60,7 @@ class Follower { * @return string */ public function __toString() { - return $this->get_actor(); - } - - /** - * Prefill the Object with the meta data. - * - * @param array $meta The meta data. - * - * @return void - */ - public function from_meta( $meta ) { - $this->meta = $meta; - - foreach ( $this->map_meta as $remote => $internal ) { - if ( ! empty( $meta[ $remote ] ) ) { - $this->$internal = $meta[ $remote ]; - } - } - - if ( ! empty( $meta['icon']['url'] ) ) { - $this->avatar = $meta['icon']['url']; - } - - if ( ! empty( $meta['endpoints']['sharedInbox'] ) ) { - $this->shared_inbox = $meta['endpoints']['sharedInbox']; - } elseif ( ! empty( $meta['inbox'] ) ) { - $this->shared_inbox = $meta['inbox']; - } - - $this->updated_at = \time(); - } - - /** - * Get the data by the given attribute - * - * @param string $attribute The attribute name. - * - * @return mixed The attribute value. - */ - public function get( $attribute ) { - if ( ! is_null( $this->$attribute ) ) { - return $this->$attribute; - } - - $attribute_value = get_post_meta( $this->id, $attribute, true ); - - if ( $attribute_value ) { - $this->$attribute = $attribute_value; - return $attribute_value; - } - - $attribute_value = $this->get_meta_by( $attribute ); - if ( $attribute_value ) { - $this->$attribute = $attribute_value; - return $attribute_value; - } - - return null; + return $this->get_url(); } /** @@ -256,8 +71,8 @@ class Follower { * @return void */ public function set_error( $error ) { - $this->errors = array(); - $this->error = $error; + $this->_errors = array(); + $this->_error = $error; } /** @@ -266,12 +81,12 @@ class Follower { * @return mixed */ public function get_errors() { - if ( $this->errors ) { - return $this->errors; + if ( $this->_errors ) { + return $this->_errors; } - $this->errors = get_post_meta( $this->id, 'errors' ); - return $this->errors; + $this->_errors = get_post_meta( $this->_id, 'errors' ); + return $this->_errors; } /** @@ -280,7 +95,7 @@ class Follower { * @return void */ public function reset_errors() { - delete_post_meta( $this->id, 'errors' ); + delete_post_meta( $this->_id, 'errors' ); } /** @@ -289,7 +104,7 @@ class Follower { * @return int The number of errors. */ public function count_errors() { - $errors = $this->get_errors(); + $errors = $this->get__errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return count( $errors ); @@ -298,13 +113,21 @@ class Follower { return 0; } + public function get_url() { + if ( ! $this->url ) { + return $this->id; + } + + return $this->url; + } + /** * Return the latest error message. * * @return string The error message. */ public function get_latest_error_message() { - $errors = $this->get_errors(); + $errors = $this->get__errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return reset( $errors ); @@ -313,35 +136,12 @@ class Follower { return ''; } - /** - * Get the meta data by the given attribute. - * - * @param string $attribute The attribute name. - * - * @return mixed $attribute The attribute value. - */ - public function get_meta_by( $attribute ) { - $meta = $this->get_meta(); - if ( ! is_array( $meta ) ) { - return null; - } - // try mapped data (see $this->map_meta) - foreach ( $this->map_meta as $remote => $local ) { - if ( $attribute === $local && isset( $meta[ $remote ] ) ) { - return $meta[ $remote ]; - } - } - - return null; - } - /** * Update the current Follower-Object. * * @return void */ public function update() { - $this->updated_at = \time(); $this->save(); } @@ -352,18 +152,18 @@ class Follower { */ public function save() { $args = array( - 'ID' => $this->id, - 'guid' => $this->actor, + 'ID' => $this->get__id(), + 'guid' => $this->get_id(), 'post_title' => $this->get_name(), 'post_author' => 0, 'post_type' => Followers::POST_TYPE, 'post_content' => $this->get_summary(), 'post_status' => 'publish', - 'post_modified' => gmdate( 'Y-m-d H:i:s', $this->updated_at ), 'meta_input' => $this->get_post_meta_input(), ); + $post_id = wp_insert_post( $args ); - $this->id = $post_id; + $this->_id = $post_id; } /** @@ -372,7 +172,7 @@ class Follower { * @return void */ public function upsert() { - if ( $this->id ) { + if ( $this->_id ) { $this->update(); } else { $this->save(); @@ -385,7 +185,7 @@ class Follower { * @return void */ public function delete() { - wp_delete_post( $this->id ); + wp_delete_post( $this->_id ); } /** @@ -394,7 +194,7 @@ class Follower { * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'name', 'username', 'meta' ); + $attributes = array( 'inbox', '_shared_inbox', 'icon', 'preferred_username', '_actor', 'url' ); $meta_input = array(); @@ -404,18 +204,94 @@ class Follower { } } - if ( $this->error ) { - if ( is_string( $this->error ) ) { - $error = $this->error; - } elseif ( is_wp_error( $this->error ) ) { - $error = $this->error->get_error_message(); + if ( $this->_error ) { + if ( is_string( $this->_error ) ) { + $_error = $this->_error; + } elseif ( is_wp_error( $this->_error ) ) { + $_error = $this->_error->get_error_message(); } else { - $error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + $_error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); } - $meta_input['errors'] = array( $error ); + $meta_input['_errors'] = $_error; } return $meta_input; } + + /** + * Get the Icon URL (Avatar) + * + * @return string The URL to the Avatar. + */ + public function get_icon_url() { + $icon = $this->get_icon(); + + if ( ! $icon ) { + return ''; + } + + if ( is_array( $icon ) ) { + return $icon['url']; + } + + return $icon; + } + + /** + * Converts an ActivityPub Array to an Follower Object. + * + * @param array $array The ActivityPub Array. + * + * @return Activitypub\Model\Follower The Follower Object. + */ + public static function from_array( $array ) { + $object = parent::from_array( $array ); + $object->set__actor( $array ); + + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $object->get_id() ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + $object->set__id( $post->ID ); + } + + if ( ! empty( $object->get_endpoints()['sharedInbox'] ) ) { + $object->_shared_inbox = $object->get_endpoints()['sharedInbox']; + } elseif ( ! empty( $object->get_inbox() ) ) { + $object->_shared_inbox = $object->get_inbox(); + } + + return $object; + } + + /** + * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. + * + * @return string The JSON string. + * + * @return array Activitypub\Model\Follower + */ + public static function from_custom_post_type( $post ) { + $object = new static(); + + $object->set__id( $post->ID ); + $object->set_id( $post->guid ); + $object->set_name( $post->post_title ); + $object->set_summary( $post->post_content ); + $object->set_url( get_post_meta( $post->ID, 'url', true ) ); + $object->set_icon( get_post_meta( $post->ID, 'icon', true ) ); + $object->set_preferred_username( get_post_meta( $post->ID, 'preferred_username', true ) ); + $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); + $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); + + return $object; + } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index fc05bdd..30509be 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -86,7 +86,7 @@ class Followers { // phpcs:ignore $json->orderedItems = array_map( function( $item ) { - return $item->get_actor(); + return $item->get_url(); }, FollowerCollection::get_followers( $user_id ) ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 2bece58..246cc0b 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -16,9 +16,9 @@ class Followers extends WP_List_Table { 'name' => \__( 'Name', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), 'identifier' => \__( 'Identifier', 'activitypub' ), - 'updated_at' => \__( 'Last updated', 'activitypub' ), - 'errors' => \__( 'Errors', 'activitypub' ), - 'latest-error' => \__( 'Latest Error Message', 'activitypub' ), + 'updated' => \__( 'Last updated', 'activitypub' ), + //'errors' => \__( 'Errors', 'activitypub' ), + //'latest-error' => \__( 'Latest Error Message', 'activitypub' ), ); } @@ -50,11 +50,11 @@ class Followers extends WP_List_Table { foreach ( $followers as $follower ) { $item = array( - 'avatar' => esc_attr( $follower->get_avatar() ), + 'icon' => esc_attr( $follower->get_icon_url() ), 'name' => esc_attr( $follower->get_name() ), - 'username' => esc_attr( $follower->get_username() ), - 'identifier' => esc_attr( $follower->get_actor() ), - 'updated_at' => esc_attr( $follower->get_updated_at() ), + 'username' => esc_attr( $follower->get_preferred_username() ), + 'identifier' => esc_attr( $follower->get_url() ), + 'updated' => esc_attr( $follower->get_updated() ), 'errors' => $follower->count_errors(), 'latest-error' => $follower->get_latest_error_message(), ); @@ -79,7 +79,7 @@ class Followers extends WP_List_Table { public function column_avatar( $item ) { return sprintf( '', - $item['avatar'] + $item['icon'] ); } diff --git a/templates/followers-list.php b/templates/followers-list.php index c79c961..7476192 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,16 +1,16 @@

    - +

    - +
    prepare_items(); - $token_table->display(); + $table->prepare_items(); + $table->display(); ?>
    diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index bde52ab..70ed304 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -2,12 +2,14 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HTTP { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', 'prefferedUsername' => 'username', ), 'jon@example.com' => array( + 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', @@ -56,6 +58,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT ); self::$users['https://example.com/alex'] = array( + 'id' => 'https://example.com/alex', 'url' => 'https://example.com/alex', 'inbox' => 'https://example.com/alex/inbox', 'name' => 'alex', diff --git a/tests/test-class-activitypub-mention.php b/tests/test-class-activitypub-mention.php index ca7395f..dce023c 100644 --- a/tests/test-class-activitypub-mention.php +++ b/tests/test-class-activitypub-mention.php @@ -2,6 +2,7 @@ class Test_Activitypub_Mention extends ActivityPub_TestCase_Cache_HTTP { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'name' => 'username', ), diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 482cecc..3634713 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -2,36 +2,42 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { public static $users = array( 'username@example.org' => array( + 'id' => 'https://example.org/users/username', 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', 'prefferedUsername' => 'username', ), 'jon@example.com' => array( + 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), 'doe@example.org' => array( + 'id' => 'https://example.org/author/doe', 'url' => 'https://example.org/author/doe', 'inbox' => 'https://example.org/author/doe/inbox', 'name' => 'doe', 'prefferedUsername' => 'doe', ), 'sally@example.org' => array( + 'id' => 'http://sally.example.org', 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', 'prefferedUsername' => 'jon', ), '12345@example.com' => array( + 'id' => 'https://12345.example.com', 'url' => 'https://12345.example.com', 'inbox' => 'https://12345.example.com/inbox', 'name' => '12345', 'prefferedUsername' => '12345', ), 'user2@example.com' => array( + 'id' => 'https://user2.example.com', 'url' => 'https://user2.example.com', 'inbox' => 'https://user2.example.com/inbox', 'name' => 'user2', @@ -66,7 +72,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $db_followers = array_map( function( $item ) { - return $item->get_actor(); + return $item->get_url(); }, $db_followers ); @@ -107,7 +113,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $this->assertNull( $follower ); @@ -116,10 +122,10 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://user2.example.com' ); - $this->assertEquals( 'https://user2.example.com', $follower2->get_actor() ); + $this->assertEquals( 'https://user2.example.com', $follower2->get_url() ); } public function test_delete_follower() { @@ -144,13 +150,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower->get_url() ); $followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 2, count( $followers ) ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_url() ); \Activitypub\Collection\Followers::remove_follower( 1, 'https://example.com/author/jon' ); @@ -158,7 +164,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertNull( $follower ); $follower2 = \Activitypub\Collection\Followers::get_follower( 2, 'https://example.com/author/jon' ); - $this->assertEquals( 'https://example.com/author/jon', $follower2->get_actor() ); + $this->assertEquals( 'https://example.com/author/jon', $follower2->get_url() ); $followers = \Activitypub\Collection\Followers::get_followers( 1 ); $this->assertEquals( 1, count( $followers ) ); @@ -174,7 +180,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Collection\Followers::add_follower( 1, $follower ); } - $follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'https://example.com/author/jon' ); global $wpdb; @@ -184,7 +190,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $post_modified = gmdate( $mysql_time_format, $time ); $post_modified_gmt = gmdate( $mysql_time_format, ( $time + get_option( 'gmt_offset' ) * HOUR_IN_SECONDS ) ); - $post_id = $follower->get_id(); + $post_id = $follower->get__id(); $wpdb->query( $wpdb->prepare( @@ -214,13 +220,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { \Activitypub\Collection\Followers::add_follower( 1, $follower ); } - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); for ( $i = 1; $i <= 15; $i++ ) { - add_post_meta( $follower->get_id(), 'errors', 'error ' . $i ); + add_post_meta( $follower->get__id(), 'errors', 'error ' . $i ); } - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $count = $follower->count_errors(); $followers = \Activitypub\Collection\Followers::get_faulty_followers(); @@ -230,7 +236,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower->reset_errors(); - $follower = new \Activitypub\Model\Follower( 'http://sally.example.org' ); + $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); $count = $follower->count_errors(); $followers = \Activitypub\Collection\Followers::get_faulty_followers(); From 2cacd374dc26e03c609f49cc0c1338c29d72570d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 23 Jun 2023 14:57:46 +0200 Subject: [PATCH 249/427] fix PHPCS issues --- includes/activity/class-activity-object.php | 5 ++++- includes/collection/class-followers.php | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/includes/activity/class-activity-object.php b/includes/activity/class-activity-object.php index 0115a40..a71a83e 100644 --- a/includes/activity/class-activity-object.php +++ b/includes/activity/class-activity-object.php @@ -524,7 +524,10 @@ class Activity_Object { $this->$key = array(); } - $this->$key[] = $value; + $attributes = $this->$key; + $attributes[] = $value; + + $this->$key = $attributes; return $this->$key; } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 96b9099..39bfc2b 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -76,9 +76,7 @@ class Followers { self::POST_TYPE, 'icon', array( - //'type' => 'string', 'single' => true, - //'sanitize_callback' => array( self::class, 'sanitize_url' ), ) ); From 6fcd19554aca43fe00d24c07ce74d22f904839e6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 23 Jun 2023 15:21:14 +0200 Subject: [PATCH 250/427] updated is not needed --- includes/collection/class-followers.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 39bfc2b..4f7022a 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -110,22 +110,6 @@ class Followers { ) ); - register_post_meta( - self::POST_TYPE, - 'updated', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function( $value ) { - if ( ! is_numeric( $value ) && (int) $value !== $value ) { - $value = \time(); - } - - return $value; - }, - ) - ); - register_post_meta( self::POST_TYPE, '_errors', @@ -292,7 +276,7 @@ class Followers { } if ( isset( $object['user_id'] ) ) { - unset( $object['_user_id'] ); + unset( $object['user_id'] ); unset( $object['@context'] ); } From ffa02e7b18e05f7b8ffd5e449829df84093890e1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 23 Jun 2023 15:41:19 +0200 Subject: [PATCH 251/427] oops --- includes/class-migration.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index cae8fc2..d3c0fcb 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -22,8 +22,7 @@ class Migration { } public static function get_version() { - return 0; - //return get_option( 'activitypub_db_version', 0 ); + return get_option( 'activitypub_db_version', 0 ); } /** From 235b5aa4a13296af6cadd99ff35516c6f1187c36 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 26 Jun 2023 11:08:04 +0200 Subject: [PATCH 252/427] build a simple to_array converter --- includes/activity/class-actor.php | 4 +-- ...ivity-object.php => class-base-object.php} | 31 +++++++++++++++++-- includes/activity/class-person.php | 2 +- includes/functions.php | 11 +++++++ tests/test-class-activitypub-activity.php | 13 ++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) rename includes/activity/{class-activity-object.php => class-base-object.php} (94%) diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index e9f810b..604261f 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -7,11 +7,11 @@ namespace Activitypub\Activity; -class Actor extends Activity_Object { +class Actor extends Base_Object { /** * @var string */ - protected $type = 'Object'; + protected $type = 'Person'; /** * A reference to an ActivityStreams OrderedCollection comprised of diff --git a/includes/activity/class-activity-object.php b/includes/activity/class-base-object.php similarity index 94% rename from includes/activity/class-activity-object.php rename to includes/activity/class-base-object.php index a71a83e..009cab3 100644 --- a/includes/activity/class-activity-object.php +++ b/includes/activity/class-base-object.php @@ -10,7 +10,7 @@ namespace Activitypub\Activity; use WP_Error; use function Activitypub\camel_to_snake_case; - +use function Activitypub\snake_to_camel_case; /** * ObjectType is an implementation of one of the * Activity Streams Core Types. @@ -23,7 +23,7 @@ use function Activitypub\camel_to_snake_case; * * @see https://www.w3.org/TR/activitystreams-core/#object */ -class Activity_Object { +class Base_Object { /** * The object's unique global identifier * @@ -562,4 +562,31 @@ class Activity_Object { return $object; } + + /** + * Convert Object to an array. + * + * It tries to get the object attributes if they exist + * and falls back to the getters. Empty values are ignored. + * + * @return array An array built from the Object. + */ + public function to_array() { + $array = array(); + $vars = get_object_vars( $this ); + + foreach ( $vars as $key => $value ) { + // if value is empty, try to get it from a getter. + if ( ! $value ) { + $value = call_user_func( array( $this, 'get_' . $key ) ); + } + + // if value is still empty, ignore it for the array and continue. + if ( $value ) { + $array[ snake_to_camel_case( $key ) ] = $value; + } + } + + return $array; + } } diff --git a/includes/activity/class-person.php b/includes/activity/class-person.php index cf161bc..f2ed9b9 100644 --- a/includes/activity/class-person.php +++ b/includes/activity/class-person.php @@ -12,7 +12,7 @@ namespace Activitypub\Activity; * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person */ -class Person extends Activity_Object { +class Person extends Base_Object { /** * @var string */ diff --git a/includes/functions.php b/includes/functions.php index 212060e..2a17bfa 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -248,6 +248,17 @@ function camel_to_snake_case( $string ) { return strtolower( preg_replace( '/(? 'https://example.com/post/123', + 'type' => 'Note', + 'content' => 'Hello world!', + ); + + $object = \Activitypub\Activity\Base_Object::from_array( $test_array ); + + $this->assertEquals( 'Hello world!', $object->get_content() ); + $this->assertEquals( $test_array, $object->to_array() ); + } } From 09518ea66b96c4955c557603b6bda18a19898611 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 16:32:00 +0200 Subject: [PATCH 253/427] prepare pseudo users like a blog wide user. this allows also other constructs like tag oder category users fix #1 --- activitypub.php | 1 + assets/css/activitypub-admin.css | 6 +- includes/class-activitypub.php | 5 + includes/class-admin.php | 15 ++ includes/class-user-factory.php | 157 ++++++++++++++ includes/functions.php | 37 +--- includes/model/class-activity.php | 15 +- includes/model/class-application-user.php | 39 ++++ includes/model/class-blog-user.php | 91 ++++++++ includes/model/class-user.php | 239 +++++++++++++++++++++- includes/rest/class-followers.php | 31 +-- includes/rest/class-following.php | 28 +-- includes/rest/class-inbox.php | 31 +-- includes/rest/class-outbox.php | 32 +-- includes/rest/class-user.php | 89 ++++++++ includes/rest/class-webfinger.php | 27 +-- includes/table/class-followers.php | 28 ++- templates/admin-header.php | 4 + templates/author-json.php | 92 +-------- templates/followers-list.php | 14 ++ 20 files changed, 744 insertions(+), 237 deletions(-) create mode 100644 includes/class-user-factory.php create mode 100644 includes/model/class-application-user.php create mode 100644 includes/model/class-blog-user.php create mode 100644 includes/rest/class-user.php diff --git a/activitypub.php b/activitypub.php index 8f665fc..0f9e8d3 100644 --- a/activitypub.php +++ b/activitypub.php @@ -39,6 +39,7 @@ function init() { Collection\Followers::init(); // Configure the REST API route + Rest\User::init(); Rest\Outbox::init(); Rest\Inbox::init(); Rest\Followers::init(); diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index cd1808c..8925bfd 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -4,6 +4,10 @@ margin-top: 10px; } +.settings_page_activitypub .wrap { + padding-left: 22px; +} + .activitypub-settings-header { text-align: center; margin: 0 0 1rem; @@ -28,7 +32,7 @@ -ms-grid-columns: 1fr 1fr; vertical-align: top; display: inline-grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr 1fr 1fr; } .activitypub-settings-tab.active { diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 55329b3..6ebfcbf 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -224,6 +224,11 @@ class Activitypub { 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2', 'top' ); + \add_rewrite_rule( + '^@([\w]+)', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', + 'top' + ); } \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); diff --git a/includes/class-admin.php b/includes/class-admin.php index 7b62a08..b1b5bc7 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,6 +13,10 @@ class Admin { * Initialize the class, registering WordPress hooks */ public static function init() { + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } + \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); @@ -24,6 +28,11 @@ class Admin { * Add admin menu entry */ public static function admin_menu() { + // user has to be able to publish posts + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } + $settings_page = \add_options_page( 'Welcome', 'ActivityPub', @@ -55,6 +64,9 @@ class Admin { case 'settings': \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; + case 'followers': + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + break; case 'welcome': default: wp_enqueue_script( 'plugin-install' ); @@ -70,6 +82,9 @@ class Admin { * Load user settings page */ public static function followers_list_page() { + if ( ! current_user_can( 'publish_posts' ) ) { + return; + } \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); } diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php new file mode 100644 index 0000000..3ae3237 --- /dev/null +++ b/includes/class-user-factory.php @@ -0,0 +1,157 @@ + 404 ) + ); + } + + return new User( $user->ID ); + } + } + + /** + * Get the User by username. + * + * @param string $username The User-Name. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_username( $username ) { + // check for blog user. + if ( get_option( 'activitypub_blog_identifier', null ) === $username ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + + // check for 'activitypub_username' meta + $user = new WP_User_Query( + array( + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => 'activitypub_identifier', + 'value' => $username, + 'compare' => 'LIKE', + ), + ), + ) + ); + + if ( $user->results ) { + return self::get_by_id( $user->results[0] ); + } + + // check for login or nicename. + $user = new WP_User_Query( + array( + 'search' => $username, + 'search_columns' => array( 'user_login', 'user_nicename' ), + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + ) + ); + + if ( $user->results ) { + return self::get_by_id( $user->results[0] ); + } + + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + /** + * Get the User by resource. + * + * @param string $resource The User-Resource. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_resource( $resource ) { + if ( \strpos( $resource, '@' ) === false ) { + return new WP_Error( + 'activitypub_unsupported_resource', + \__( 'Resource is invalid', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $resource = \str_replace( 'acct:', '', $resource ); + + $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); + $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + + if ( $blog_host !== $resource_host ) { + return new WP_Error( + 'activitypub_wrong_host', + \__( 'Resource host does not match blog host', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + return self::get_by_username( $resource_identifier ); + } + + /** + * Get the User by resource. + * + * @param string $resource The User-Resource. + * + * @return \Acitvitypub\Model\User The User. + */ + public static function get_by_various( $id ) { + if ( is_numeric( $id ) ) { + return self::get_by_id( $id ); + } elseif ( filter_var( $id, FILTER_VALIDATE_URL ) ) { + return self::get_by_resource( $id ); + } else { + return self::get_by_username( $id ); + } + } +} diff --git a/includes/functions.php b/includes/functions.php index 2a17bfa..8811d9b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -7,27 +7,7 @@ namespace Activitypub; * @return array the activitypub context */ function get_context() { - $context = array( - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - array( - 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', - 'PropertyValue' => 'schema:PropertyValue', - 'schema' => 'http://schema.org#', - 'pt' => 'https://joinpeertube.org/ns#', - 'toot' => 'http://joinmastodon.org/ns#', - 'value' => 'schema:value', - 'Hashtag' => 'as:Hashtag', - 'featured' => array( - '@id' => 'toot:featured', - '@type' => '@id', - ), - 'featuredTags' => array( - '@id' => 'toot:featuredTags', - '@type' => '@id', - ), - ), - ); + $context = Model\Activity::CONTEXT; return \apply_filters( 'activitypub_json_context', $context ); } @@ -187,21 +167,6 @@ function url_to_authorid( $url ) { return 0; } -/** - * Return the custom Activity Pub description, if set, or default author description. - * - * @param int $user_id The user ID. - * - * @return string The author description. - */ -function get_author_description( $user_id ) { - $description = get_user_meta( $user_id, 'activitypub_user_description', true ); - if ( empty( $description ) ) { - $description = get_user_meta( $user_id, 'description', true ); - } - return \wpautop( \wp_kses( $description, 'default' ) ); -} - /** * Check for Tombstone Objects * diff --git a/includes/model/class-activity.php b/includes/model/class-activity.php index bf06bc1..bef7f1e 100644 --- a/includes/model/class-activity.php +++ b/includes/model/class-activity.php @@ -11,12 +11,8 @@ use function Activitypub\get_rest_url_by_path; * @see https://www.w3.org/TR/activitypub/ */ class Activity { - /** - * The JSON-LD context. - * - * @var array - */ - private $context = array( + + const CONTEXT = array( 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', array( @@ -38,6 +34,13 @@ class Activity { ), ); + /** + * The JSON-LD context. + * + * @var array + */ + private $context = self::CONTEXT; + /** * The published date. * diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php new file mode 100644 index 0000000..c800531 --- /dev/null +++ b/includes/model/class-application-user.php @@ -0,0 +1,39 @@ + 'date', + 'order' => 'ASC', + 'number' => 1, + ) + ); + + if ( ! empty( $first_post->posts[0] ) ) { + $time = \strtotime( $first_post->posts[0]->post_date_gmt ); + } else { + $time = \time(); + } + + return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + } +} diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 5c62473..ba24a66 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -1,23 +1,242 @@ user_id = $user_id; + + add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + add_filter( 'activitypub_json_author_array', array( $this, 'add_attachments' ), 10, 2 ); + } + + /** + * Magic function to implement getter and setter + * + * @param string $method + * @param string $params + * + * @return void + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + return $this->$var; + } + + if ( \strncasecmp( $method, 'has', 3 ) === 0 ) { + return (bool) call_user_func( 'get_' . $var, $this ); + } + } + + /** + * Get the User-ID. + * + * @return string The User-ID. + */ + public function get_id() { + return $this->get_url(); + } + + /** + * Get the User-Name. + * + * @return string The User-Name. + */ + public function get_name() { + return \esc_attr( \get_the_author_meta( 'display_name', $this->user_id ) ); + } + + /** + * Get the User-Description. + * + * @return string The User-Description. + */ + public function get_summary() { + $description = get_user_meta( $this->user_id, 'activitypub_user_description', true ); + if ( empty( $description ) ) { + $description = get_user_meta( $this->user_id, 'description', true ); + } + return \wpautop( \wp_kses( $description, 'default' ) ); + } + + /** + * Get the User-Url. + * + * @return string The User-Url. + */ + public function get_url() { + return \esc_url( \get_author_posts_url( $this->user_id ) ); + } + + public function get_username() { + return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); + } + + public function get_avatar() { + return \esc_url( + \get_avatar_url( + $this->user_id, + array( 'size' => 120 ) + ) + ); + } + + public function get_header_image() { + if ( \has_header_image() ) { + return \esc_url( \get_header_image() ); + } + + return null; + } + + public function get_published() { + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->user_id ) ) ); + } + + public function get_public_key() { + //return Signature::get_public_key( $this->user_id ); + return null; + } + + /** + * Array representation of the User. + * + * @param bool $context Whether to include the @context. + * + * @return array The array representation of the User. + */ + public function to_array( $context = true ) { + $output = array(); + + if ( $context ) { + $output['@context'] = Activity::CONTEXT; + } + + $output['id'] = $this->get_url(); + $output['type'] = $this->get_type(); + $output['name'] = $this->get_name(); + $output['summary'] = \html_entity_decode( + $this->get_summary(), + \ENT_QUOTES, + 'UTF-8' + ); + $output['preferredUsername'] = $this->get_username(); // phpcs:ignore + $output['url'] = $this->get_url(); + $output['icon'] = array( + 'type' => 'Image', + 'url' => $this->get_avatar(), + ); + + if ( $this->has_header_image() ) { + $output['image'] = array( + 'type' => 'Image', + 'url' => $this->get_header_image(), + ); + } + + $output['published'] = $this->get_published(); + + $output['publicKey'] = array( + 'id' => $this->get_url() . '#main-key', + 'owner' => $this->get_url(), + 'publicKeyPem' => \trim( $this->get_public_key() ), + ); + + $output['manuallyApprovesFollowers'] = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore + + // filter output + $output = \apply_filters( 'activitypub_json_author_array', $output, $this->user_id, $this ); + + return $output; + } + + /** + * Extend the User-Output with API-Endpoints. + * + * @param array $array The User-Output. + * @param numeric $user_id The User-ID. + * + * @return array The extended User-Output. + */ + public function add_api_endpoints( $array, $user_id ) { + $array['inbox'] = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); + $array['outbox'] = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); + $array['followers'] = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); + $array['following'] = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); + + return $array; + } + + /** + * Extend the User-Output with Attachments. + * + * @param array $array The User-Output. + * @param numeric $user_id The User-ID. + * + * @return array The extended User-Output. + */ + public function add_attachments( $array, $user_id ) { + $array['attachment'] = array(); + + $array['attachment']['blog_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Blog', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + $array['attachment']['profile_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Profile', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \get_author_posts_url( $user_id ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + if ( \get_the_author_meta( 'user_url', $user_id ) ) { + $array['attachment']['user_url'] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Website', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \get_the_author_meta( 'user_url', $user_id ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + } + + return $array; + } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 30509be..233c4ed 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,6 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; +use Activitypub\User_Factory; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -30,7 +31,7 @@ class Followers { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/followers', + '/users/(?P\w+)/followers', array( array( 'methods' => WP_REST_Server::READABLE, @@ -51,19 +52,10 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = \get_user_by( 'ID', $user_id ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $user ) { - return new WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } /* @@ -77,18 +69,18 @@ class Followers { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get_user_id() ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore - $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user->get_user_id() ); // phpcs:ignore // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user_id ) + FollowerCollection::get_followers( $user->get_user_id() ) ); $response = new WP_REST_Response( $json, 200 ); @@ -111,10 +103,7 @@ class Followers { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 93ad6a7..416d3a4 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,6 +1,8 @@ \d+)/following', + '/users/(?P\w+)/following', array( array( 'methods' => \WP_REST_Server::READABLE, @@ -45,19 +47,10 @@ class Following { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = \get_user_by( 'ID', $user_id ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $user ) { - return new \WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } /* @@ -71,10 +64,10 @@ class Following { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore @@ -100,10 +93,7 @@ class Following { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 76ce57c..8e77edb 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Signature; +use Activitypub\User_Factory; use Activitypub\Model\Activity; use function Activitypub\get_context; @@ -48,7 +48,7 @@ class Inbox { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/inbox', + '/users/(?P\w+)/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, @@ -74,6 +74,12 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + $page = $request->get_param( 'page', 0 ); /* @@ -87,7 +93,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore @@ -120,13 +126,18 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } $data = $request->get_params(); $type = $request->get_param( 'type' ); $type = \strtolower( $type ); - \do_action( 'activitypub_inbox', $data, $user_id, $type ); - \do_action( "activitypub_inbox_{$type}", $data, $user_id ); + \do_action( 'activitypub_inbox', $data, $user->get_user_id(), $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user->get_user_id() ); return new WP_REST_Response( array(), 202 ); } @@ -185,10 +196,7 @@ class Inbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; @@ -208,10 +216,7 @@ class Inbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); $params['id'] = array( diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 2138f71..d7e730f 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,6 +5,7 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; +use Activitypub\User_Factory; use Activitypub\Model\Post; use Activitypub\Model\Activity; @@ -32,7 +33,7 @@ class Outbox { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\d+)/outbox', + '/users/(?P\w+)/outbox', array( array( 'methods' => WP_REST_Server::READABLE, @@ -52,22 +53,14 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $author = \get_user_by( 'ID', $user_id ); - $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); + $user = User_Factory::get_by_various( $user_id ); - if ( ! $author ) { - return new WP_Error( - 'rest_invalid_param', - \__( 'User not found', 'activitypub' ), - array( - 'status' => 404, - 'params' => array( - 'user_id' => \__( 'User not found', 'activitypub' ), - ), - ) - ); + if ( is_wp_error( $user ) ) { + return $user; } + $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); + $page = $request->get_param( 'page', 0 ); /* @@ -80,9 +73,9 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = \get_author_posts_url( $user_id ); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get_user_id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -104,7 +97,7 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user_id, + 'author' => $user->get_user_id(), 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) @@ -148,10 +141,7 @@ class Outbox { $params['user_id'] = array( 'required' => true, - 'type' => 'integer', - 'validate_callback' => function( $param, $request, $key ) { - return user_can( $param, 'publish_posts' ); - }, + 'type' => 'string', ); return $params; diff --git a/includes/rest/class-user.php b/includes/rest/class-user.php new file mode 100644 index 0000000..4bb3783 --- /dev/null +++ b/includes/rest/class-user.php @@ -0,0 +1,89 @@ +\w+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( self::class, 'get' ), + 'args' => self::request_parameters(), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Handle GET request + * + * @param WP_REST_Request $request + * + * @return WP_REST_Response + */ + public static function get( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = User_Factory::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_outbox_pre' ); + + $json = $user->to_array(); + + $response = new WP_REST_Response( $json, 200 ); + $response->header( 'Content-Type', 'application/activity+json' ); + + return $response; + } + + /** + * The supported parameters + * + * @return array list of parameters + */ + public static function request_parameters() { + $params = array(); + + $params['page'] = array( + 'type' => 'string', + ); + + $params['user_id'] = array( + 'required' => true, + 'type' => 'string', + ); + + return $params; + } +} diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f75a3f7..4f1d5cf 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,6 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; +use Activitypub\User_Factory; /** * ActivityPub WebFinger REST-Class @@ -47,41 +48,27 @@ class Webfinger { public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - if ( \strpos( $resource, '@' ) === false ) { - return new WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) ); - } + $user = User_Factory::get_by_resource( $resource ); - $resource = \str_replace( 'acct:', '', $resource ); - - $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); - $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); - - if ( $blog_host !== $resource_host ) { - return new WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); - } - - $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); - - if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { - return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); + if ( is_wp_error( $user ) ) { + return $user; } $json = array( 'subject' => $resource, 'aliases' => array( - \get_author_posts_url( $user->ID ), + $user->get_url(), ), 'links' => array( array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ), array( 'rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ), ), ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 246cc0b..5f1c199 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -9,6 +9,24 @@ if ( ! \class_exists( '\WP_List_Table' ) ) { } class Followers extends WP_List_Table { + private $user_id; + + public function __construct() { + if ( get_current_screen()->id === 'settings_page_activitypub' ) { + $this->user_id = -1; + } else { + $this->user_id = \get_current_user_id(); + } + + parent::__construct( + array( + 'singular' => \__( 'Follower', 'activitypub' ), + 'plural' => \__( 'Followers', 'activitypub' ), + 'ajax' => false, + ) + ); + } + public function get_columns() { return array( 'cb' => '', @@ -36,8 +54,8 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $followers = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page ); - $counter = FollowerCollection::count_followers( \get_current_user_id() ); + $follower = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); $this->set_pagination_args( @@ -104,7 +122,7 @@ class Followers extends WP_List_Table { return false; } - if ( ! current_user_can( 'edit_user', \get_current_user_id() ) ) { + if ( ! current_user_can( 'edit_user', $this->user_id ) ) { return false; } @@ -121,4 +139,8 @@ class Followers extends WP_List_Table { break; } } + + public function get_user_count() { + return FollowerCollection::count_followers( $this->user_id ); + } } diff --git a/templates/admin-header.php b/templates/admin-header.php index 73a830b..974b224 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -11,6 +11,10 @@ + + + +

    diff --git a/templates/author-json.php b/templates/author-json.php index 7b112ac..aa38b97 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,92 +1,10 @@ {'@context'} = \Activitypub\get_context(); -$json->id = \get_author_posts_url( $author_id ); -$json->type = 'Person'; -$json->name = \get_the_author_meta( 'display_name', $author_id ); -$json->summary = \html_entity_decode( - \Activitypub\get_author_description( $author_id ), - \ENT_QUOTES, - 'UTF-8' -); -$json->preferredUsername = \get_the_author_meta( 'login', $author_id ); // phpcs:ignore -$json->url = \get_author_posts_url( $author_id ); -$json->icon = array( - 'type' => 'Image', - 'url' => \get_avatar_url( $author_id, array( 'size' => 120 ) ), -); - -$json->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $author_id ) ) ); - -if ( \has_header_image() ) { - $json->image = array( - 'type' => 'Image', - 'url' => \get_header_image(), - ); -} - -$json->inbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) ); -$json->outbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) ); -$json->followers = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) ); -$json->following = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/following', $author_id ) ); - -$json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - -// phpcs:ignore -$json->publicKey = array( - 'id' => \get_author_posts_url( $author_id ) . '#main-key', - 'owner' => \get_author_posts_url( $author_id ), - 'publicKeyPem' => \trim( \Activitypub\Signature::get_public_key( $author_id ) ), -); - -$json->tag = array(); -$json->attachment = array(); - -$json->attachment['blog_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -$json->attachment['profile_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Profile', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_author_posts_url( $author_id ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -if ( \get_the_author_meta( 'user_url', $author_id ) ) { - $json->attachment['user_url'] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Website', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_the_author_meta( 'user_url', $author_id ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), - ); -} - -// filter output -$json = \apply_filters( 'activitypub_json_author_array', $json, $author_id ); - -// migrate to ActivityPub standard -$json->attachment = array_values( $json->attachment ); +$user = \Activitypub\User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $author_id ); +\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -101,12 +19,12 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $author_id ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); \header( 'Content-Type: application/activity+json' ); -echo \wp_json_encode( $json, $options ); +echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $author_id ); +\do_action( 'activitypub_json_author_post', $user->get_user_id() ); diff --git a/templates/followers-list.php b/templates/followers-list.php index 7476192..733753e 100644 --- a/templates/followers-list.php +++ b/templates/followers-list.php @@ -1,3 +1,17 @@ +id === 'settings_page_activitypub' ) { + \load_template( + \dirname( __FILE__ ) . '/admin-header.php', + true, + array( + 'settings' => '', + 'welcome' => '', + 'followers' => 'active', + ) + ); +} +?> +

    From 03f2c2489249c133bf54bc77f94eae9428250914 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 17:27:46 +0200 Subject: [PATCH 254/427] small improvements --- activitypub.php | 2 +- includes/class-activitypub.php | 1 + includes/model/class-blog-user.php | 4 ++++ includes/model/class-user.php | 4 ++-- includes/rest/{class-user.php => class-users.php} | 2 +- includes/rest/class-webfinger.php | 3 +-- 6 files changed, 10 insertions(+), 6 deletions(-) rename includes/rest/{class-user.php => class-users.php} (99%) diff --git a/activitypub.php b/activitypub.php index 0f9e8d3..81639b8 100644 --- a/activitypub.php +++ b/activitypub.php @@ -39,7 +39,7 @@ function init() { Collection\Followers::init(); // Configure the REST API route - Rest\User::init(); + Rest\Users::init(); Rest\Outbox::init(); Rest\Inbox::init(); Rest\Followers::init(); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 6ebfcbf..ae178d3 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -97,6 +97,7 @@ class Activitypub { return $template; } } + return $json_template; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index e237fcc..c8df74d 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -88,4 +88,8 @@ class Blog_User extends User { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } + + public function get_public_key() { + return ''; + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index ba24a66..29b65bf 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -3,6 +3,7 @@ namespace Activitypub\Model; use WP_Query; use WP_Error; +use Activitypub\Signature; use Activitypub\Model\User; use Activitypub\User_Factory; @@ -121,8 +122,7 @@ class User { } public function get_public_key() { - //return Signature::get_public_key( $this->user_id ); - return null; + return Signature::get_public_key( $this->user_id ); } /** diff --git a/includes/rest/class-user.php b/includes/rest/class-users.php similarity index 99% rename from includes/rest/class-user.php rename to includes/rest/class-users.php index 4bb3783..56a10bc 100644 --- a/includes/rest/class-user.php +++ b/includes/rest/class-users.php @@ -13,7 +13,7 @@ use Activitypub\User_Factory; * * @see https://www.w3.org/TR/activitypub/#followers */ -class User { +class Users { /** * Initialize the class, registering WordPress hooks */ diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 4f1d5cf..9b07a95 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -47,8 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - - $user = User_Factory::get_by_resource( $resource ); + $user = User_Factory::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; From a1791b963c4b77bbacc3e1ae5ec578859acff61e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 24 May 2023 17:40:48 +0200 Subject: [PATCH 255/427] try new id urls --- includes/model/class-application-user.php | 4 ++++ includes/model/class-blog-user.php | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index c800531..b5623ed 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -36,4 +36,8 @@ class Application_User extends Blog_User { public function get_url() { return ''; } + + public function get_name() { + return \esc_html( \get_bloginfo( 'activitypub_application_identifier', 'application' ) ); + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c8df74d..435fde8 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -52,11 +52,11 @@ class Blog_User extends User { * @return string The User-Url. */ public function get_url() { - return ''; + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } public function get_username() { - return ''; + return \esc_html( \get_bloginfo( 'activitypub_blog_identifier', 'feed' ) ); } public function get_avatar() { From f8b93760df767038f528c52a31bd18e8470f7bde Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 10:30:51 +0200 Subject: [PATCH 256/427] fix copy&paste issue thanks @mattwiebe --- includes/model/class-application-user.php | 2 +- includes/model/class-blog-user.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index b5623ed..77ba7fb 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -38,6 +38,6 @@ class Application_User extends Blog_User { } public function get_name() { - return \esc_html( \get_bloginfo( 'activitypub_application_identifier', 'application' ) ); + return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 435fde8..944fe6d 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -56,7 +56,7 @@ class Blog_User extends User { } public function get_username() { - return \esc_html( \get_bloginfo( 'activitypub_blog_identifier', 'feed' ) ); + return \esc_html( \get_option( 'activitypub_blog_identifier', 'feed' ) ); } public function get_avatar() { From 3feef1e8cf89181d8273a1a53ee6c342cb0fe740 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 25 May 2023 13:55:18 +0200 Subject: [PATCH 257/427] send user and blog activities and set the blog to "single-mode" --- activitypub.php | 1 + includes/class-activity-dispatcher.php | 25 +++++++++++++++++++++++-- includes/functions.php | 9 +++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/activitypub.php b/activitypub.php index 81639b8..7bc4306 100644 --- a/activitypub.php +++ b/activitypub.php @@ -28,6 +28,7 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); + \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) || \define( 'ACTIVITYPUB_SINGLE_USER_MODE', false ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index ee435a7..811f923 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; +use Activitypub\User_Factory; use Activitypub\Collection\Followers; use function Activitypub\safe_remote_post; @@ -66,12 +67,32 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + if ( ! is_single_user_mode() ) { + // send User-Activity + self::send_user_activity( $activitypub_post, $activity_type ); + } + + // send Blog-User-Activity + self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); + } + + public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { + if ( $user_id ) { + $user = User_Factory::get_by_id( $user_id ); + $actor = $user->get_url(); + } else { + // get latest version of post + $user_id = $activitypub_post->get_post_author(); + $actor = null; + } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); + if ( $actor ) { + $activitypub_activity->set_actor( $actor ); + } + $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/functions.php b/includes/functions.php index 8811d9b..1f25e04 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -267,3 +267,12 @@ function is_activitypub_request() { return false; } + +/** + * Check if the current site is in single-user mode. + * + * @return boolean + */ +function is_single_user_mode() { + return ACTIVITYPUB_SINGLE_USER_MODE; +} From 503353bcd0f31f6bca6078e89063c8a75b2043c2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 26 May 2023 16:08:08 +0200 Subject: [PATCH 258/427] Added settings for blog-wide user --- assets/css/activitypub-admin.css | 16 +++++- includes/class-admin.php | 33 ++++++++++- includes/class-user-factory.php | 9 ++- includes/class-webfinger.php | 17 ++++-- includes/model/class-blog-user.php | 2 +- includes/rest/class-followers.php | 2 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 2 +- includes/rest/class-outbox.php | 2 +- includes/rest/class-users.php | 2 +- templates/settings.php | 90 +++++++++++++++++++++++++++--- 11 files changed, 152 insertions(+), 25 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 8925bfd..db4546c 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -1,3 +1,8 @@ +.activitypub-settings-body { + max-width: 800px; + margin: 0 auto; +} + .settings_page_activitypub .notice { max-width: 800px; margin: auto; @@ -115,7 +120,8 @@ summary { flex-grow: 1; } -.activitypub-settings-accordion-trigger .icon, .activitypub-settings-accordion-viewed .icon { +.activitypub-settings-accordion-trigger .icon, +.activitypub-settings-accordion-viewed .icon { border: solid #50575e medium; border-width: 0 2px 2px 0; height: .5rem; @@ -131,7 +137,8 @@ summary { transform: translateY(-30%) rotate(-135deg); } -.activitypub-settings-accordion-trigger:active, .activitypub-settings-accordion-trigger:hover { +.activitypub-settings-accordion-trigger:active, +.activitypub-settings-accordion-trigger:hover { background: #f6f7f7; } @@ -143,3 +150,8 @@ summary { outline: 2px solid #2271b1; background-color: #f6f7f7; } + +.activitypub-settings-body +input.blog-user-identifier { + text-align: right; +} diff --git a/includes/class-admin.php b/includes/class-admin.php index b1b5bc7..b88a8de 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -100,7 +100,11 @@ class Admin { 'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ), 'show_in_rest' => array( 'schema' => array( - 'enum' => array( 'title', 'excerpt', 'content' ), + 'enum' => array( + 'title', + 'excerpt', + 'content', + ), ), ), 'default' => 'content', @@ -133,7 +137,11 @@ class Admin { 'description' => \__( 'The Activity-Object-Type', 'activitypub' ), 'show_in_rest' => array( 'schema' => array( - 'enum' => array( 'note', 'article', 'wordpress-post-format' ), + 'enum' => array( + 'note', + 'article', + 'wordpress-post-format', + ), ), ), 'default' => 'note', @@ -158,6 +166,27 @@ class Admin { 'default' => array( 'post', 'pages' ), ) ); + \register_setting( + 'activitypub', + 'activitypub_blog_user_identifier', + array( + 'type' => 'string', + 'description' => \esc_html__( 'The Identifier of th Blog-User', 'activitypub' ), + 'show_in_rest' => true, + 'default' => 'feed', + 'sanitize_callback' => function( $value ) { + // hack to allow dots in the username + $parts = explode( '.', $value ); + $sanitized = array(); + + foreach ( $parts as $part ) { + $sanitized[] = \sanitize_title( $part ); + } + + return implode( '.', $sanitized ); + }, + ) + ); } public static function add_settings_help_tab() { diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 3ae3237..faa170e 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -58,10 +58,15 @@ class User_Factory { */ public static function get_by_username( $username ) { // check for blog user. - if ( get_option( 'activitypub_blog_identifier', null ) === $username ) { + if ( get_option( 'activitypub_blog_user_identifier', null ) === $username ) { return self::get_by_id( self::BLOG_USER_ID ); } + // check for application user. + if ( get_option( 'activitypub_application_user_identifier', null ) === $username ) { + return self::get_by_id( self::APPLICATION_USER_ID ); + } + // check for 'activitypub_username' meta $user = new WP_User_Query( array( @@ -71,7 +76,7 @@ class User_Factory { 'meta_query' => array( 'relation' => 'OR', array( - 'key' => 'activitypub_identifier', + 'key' => 'activitypub_user_identifier', 'value' => $username, 'compare' => 'LIKE', ), diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 1581853..44a4a27 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -32,18 +32,25 @@ class Webfinger { return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } - public static function resolve( $account ) { - if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $account, $m ) ) { + /** + * Resolve a WebFinger resource + * + * @param string $resource The WebFinger resource + * + * @return string|WP_Error The URL or WP_Error + */ + public static function resolve( $resource ) { + if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $resource, $m ) ) { return null; } - $transient_key = 'activitypub_resolve_' . ltrim( $account, '@' ); + $transient_key = 'activitypub_resolve_' . ltrim( $resource, '@' ); $link = \get_transient( $transient_key ); if ( $link ) { return $link; } - $url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' ); + $url = \add_query_arg( 'resource', 'acct:' . ltrim( $resource, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' ); if ( ! \wp_http_validate_url( $url ) ) { $response = new WP_Error( 'invalid_webfinger_url', null, $url ); \set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period. @@ -82,7 +89,7 @@ class Webfinger { } } - $link = new WP_Error( 'webfinger_url_no_activity_pub', null, $body ); + $link = new WP_Error( 'webfinger_url_no_activitypub', null, $body ); \set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period. return $link; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 944fe6d..2183efb 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -56,7 +56,7 @@ class Blog_User extends User { } public function get_username() { - return \esc_html( \get_option( 'activitypub_blog_identifier', 'feed' ) ); + return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); } public function get_avatar() { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 233c4ed..792cdfb 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -31,7 +31,7 @@ class Followers { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/followers', + '/users/(?P[\w\-\.]+)/followers', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 416d3a4..0d1de61 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -26,7 +26,7 @@ class Following { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/following', + '/users/(?P[\w\-\.]+)/following', array( array( 'methods' => \WP_REST_Server::READABLE, diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 8e77edb..54e2894 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -48,7 +48,7 @@ class Inbox { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/inbox', + '/users/(?P[\w\-\.]+)/inbox', array( array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index d7e730f..cb09cfd 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -33,7 +33,7 @@ class Outbox { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)/outbox', + '/users/(?P[\w\-\.]+)/outbox', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 56a10bc..c6d92ee 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -27,7 +27,7 @@ class Users { public static function register_routes() { \register_rest_route( ACTIVITYPUB_REST_NAMESPACE, - '/users/(?P\w+)', + '/users/(?P[\w\-\.]+)', array( array( 'methods' => WP_REST_Server::READABLE, diff --git a/templates/settings.php b/templates/settings.php index 0daca17..819dbb0 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -9,7 +9,7 @@ ); ?> -
    +

    +

    + +

    + + + + + + + + +
    + + + +

    + +

    +
    + + +

    @@ -42,16 +67,44 @@

    - - +

    - - +

    - - +

    - - +

    @@ -98,13 +151,34 @@

    - - +

    - - +

    - - +

    From a617553ddfe4e6e4cc3e15a3e53677b527cd75aa Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:00:48 +0200 Subject: [PATCH 259/427] fix profile pages --- includes/class-activitypub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index ae178d3..72cea52 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -226,8 +226,8 @@ class Activitypub { 'top' ); \add_rewrite_rule( - '^@([\w]+)', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', + '^@([\w\-\.]+)', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]&redirect=true', 'top' ); } From c95e501f98a5708f09258f9592c80c686d33742a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:22:01 +0200 Subject: [PATCH 260/427] redirect to canonical URL if it is not an ActivityPub request --- includes/class-activitypub.php | 2 +- includes/functions.php | 2 +- includes/model/class-blog-user.php | 4 ++++ includes/model/class-user.php | 4 ++++ includes/rest/class-users.php | 10 +++++++++- 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 72cea52..2da2dce 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -227,7 +227,7 @@ class Activitypub { ); \add_rewrite_rule( '^@([\w\-\.]+)', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]&redirect=true', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]', 'top' ); } diff --git a/includes/functions.php b/includes/functions.php index 1f25e04..dfbdf37 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -236,7 +236,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( 'REST_REQUEST' ) && ! REST_REQUEST ) { return false; } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 2183efb..fd01aa7 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -55,6 +55,10 @@ class Blog_User extends User { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } + public function get_canonical_url() { + return \get_home_url(); + } + public function get_username() { return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 29b65bf..5bb1e77 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -96,6 +96,10 @@ class User { return \esc_url( \get_author_posts_url( $this->user_id ) ); } + public function get_canonical_url() { + return $this->get_url(); + } + public function get_username() { return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index c6d92ee..8c0452d 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -6,6 +6,8 @@ use WP_REST_Server; use WP_REST_Response; use Activitypub\User_Factory; +use function Activitypub\is_activitypub_request; + /** * ActivityPub Followers REST-Class * @@ -54,6 +56,12 @@ class Users { return $user; } + // redirect to canonical URL if it is not an ActivityPub request + if ( ! is_activitypub_request() ) { + header( 'Location: ' . $user->get_canonical_url() ); + exit; + } + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -81,7 +89,7 @@ class Users { $params['user_id'] = array( 'required' => true, - 'type' => 'string', + 'type' => 'string', ); return $params; From daf228fd44af2906837c4c343aae47f55685a6a1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 10:29:23 +0200 Subject: [PATCH 261/427] move permanently --- includes/rest/class-users.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 8c0452d..66637b7 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -58,7 +58,7 @@ class Users { // redirect to canonical URL if it is not an ActivityPub request if ( ! is_activitypub_request() ) { - header( 'Location: ' . $user->get_canonical_url() ); + header( 'Location: ' . $user->get_canonical_url(), true, 301 ); exit; } From 2feca1388ad007e6131ab660803ec81cf55e7edf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 11:22:20 +0200 Subject: [PATCH 262/427] generate default username --- includes/class-user-factory.php | 2 +- includes/model/class-blog-user.php | 49 +++++++++++++++++++++- templates/blog-json.php | 66 +++--------------------------- templates/settings.php | 2 +- 4 files changed, 55 insertions(+), 64 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index faa170e..8ba4521 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -58,7 +58,7 @@ class User_Factory { */ public static function get_by_username( $username ) { // check for blog user. - if ( get_option( 'activitypub_blog_user_identifier', null ) === $username ) { + if ( Blog_User::get_default_username() === $username ) { return self::get_by_id( self::BLOG_USER_ID ); } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index fd01aa7..492bf76 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -59,8 +59,55 @@ class Blog_User extends User { return \get_home_url(); } + /** + * Generate and save a default Username. + * + * @return string The auto-generated Username. + */ + public static function get_default_username() { + $username = \get_option( 'activitypub_blog_user_identifier' ); + + if ( $username ) { + return $username; + } + + // check if domain host has a subdomain + $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); + $host = \str_replace( 'www.', '', $host ); + $host_parts = \explode( '.', $host ); + + if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { + \update_option( 'activitypub_blog_user_identifier', $host ); + return $host; + } + + // check blog title + $blog_title = \get_bloginfo( 'name' ); + $blog_title = \sanitize_title( $blog_title ); + + if ( strlen( $blog_title ) <= 15 ) { + \update_option( 'activitypub_blog_user_identifier', $blog_title ); + return $blog_title; + } + + $default_identifier = array( + 'feed', + 'all', + 'everyone', + 'authors', + 'follow', + 'posts', + ); + + // get random item of $default_identifier + $default = $default_identifier[ \array_rand( $default_identifier ) ]; + \update_option( 'activitypub_blog_user_identifier', $default ); + + return $default; + } + public function get_username() { - return \esc_html( \get_option( 'activitypub_blog_user_identifier', 'feed' ) ); + return self::get_default_username(); } public function get_avatar() { diff --git a/templates/blog-json.php b/templates/blog-json.php index b87bc94..cfa759c 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,66 +1,10 @@ {'@context'} = \Activitypub\get_context(); -$json->id = \get_home_url( '/' ); -$json->type = 'Organization'; -$json->name = \get_bloginfo( 'name' ); -$json->summary = \html_entity_decode( - \get_bloginfo( 'description' ), - \ENT_QUOTES, - 'UTF-8' -); -$json->preferredUsername = \get_bloginfo( 'name' ); // phpcs:ignore -$json->url = \get_home_url( '/' ); - -if ( \has_site_icon() ) { - $json->icon = array( - 'type' => 'Image', - 'url' => \get_site_icon_url( 120 ), - ); -} - -if ( \has_header_image() ) { - $json->image = array( - 'type' => 'Image', - 'url' => \get_header_image(), - ); -} - -$json->inbox = \Activitypub\get_rest_url_by_path( 'blog/inbox' ); -$json->outbox = \Activitypub\get_rest_url_by_path( 'blog/outbox' ); -$json->followers = \Activitypub\get_rest_url_by_path( 'blog/followers' ); -$json->following = \Activitypub\get_rest_url_by_path( 'blog/following' ); - -$json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - -// phpcs:ignore -$json->publicKey = array( - 'id' => \get_home_url( '/' ) . '#main-key', - 'owner' => \get_home_url( '/' ), - 'publicKeyPem' => '', -); - -$json->tag = array(); -$json->attachment = array(); - -$json->attachment[] = array( - 'type' => 'PropertyValue', - 'name' => \__( 'Blog', 'activitypub' ), - 'value' => \html_entity_decode( - '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', - \ENT_QUOTES, - 'UTF-8' - ), -); - -// filter output -$json = \apply_filters( 'activitypub_json_blog_array', $json ); +$user = \Activitypub\User_Factory::get_by_id( 0 ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_blog_pre' ); +\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -75,12 +19,12 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_blog_options', $options ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); \header( 'Content-Type: application/activity+json' ); -echo \wp_json_encode( $json, $options ); +echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_blog_post' ); +\do_action( 'activitypub_json_author_post', $user->get_user_id() ); diff --git a/templates/settings.php b/templates/settings.php index 819dbb0..08a65a1 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -42,7 +42,7 @@

    From 7b9b3dbc3771039f5387b87a9524117f48d6db2d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 11:37:21 +0200 Subject: [PATCH 263/427] add @-urls to webfinger aliases --- includes/model/class-user.php | 4 ++++ includes/rest/class-webfinger.php | 12 ++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 5bb1e77..2cee714 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -100,6 +100,10 @@ class User { return $this->get_url(); } + public function get_at_url() { + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); + } + public function get_username() { return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); } diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 9b07a95..ec76686 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -53,12 +53,16 @@ class Webfinger { return $user; } + $aliases = array( + $user->get_url(), + $user->get_canonical_url(), + $user->get_at_url(), + ); + $json = array( 'subject' => $resource, - 'aliases' => array( - $user->get_url(), - ), - 'links' => array( + 'aliases' => array_values( array_unique( $aliases ) ), + 'links' => array( array( 'rel' => 'self', 'type' => 'application/activity+json', From 4d8170413bec2270d2c0eb3a6dbe29af6c70decf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 30 May 2023 15:19:05 +0200 Subject: [PATCH 264/427] avatar and header-image settings --- assets/css/activitypub-admin.css | 21 +++++++++++++++++++++ includes/class-activitypub.php | 29 +++++++++++++++++++++++++++++ templates/settings.php | 20 +++++++++++++++++++- 3 files changed, 69 insertions(+), 1 deletion(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index db4546c..805aaa5 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -155,3 +155,24 @@ summary { input.blog-user-identifier { text-align: right; } + +.activitypub-settings-body +.header-image { + width: 100%; + height: 80px; + position: relative; + display: block; + margin-bottom: 40px; + background-image: rgb(168,165,175); + background-image: linear-gradient(180deg, red, yellow); + background-size: cover; +} + +.activitypub-settings-body +.logo { + height: 80px; + width: 80px; + position: relative; + top: 40px; + left: 40px; +} diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 2da2dce..7e5ea2f 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -28,6 +28,8 @@ class Activitypub { \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); \add_action( 'init', array( self::class, 'add_rewrite_rules' ) ); + + \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 ); } /** @@ -242,4 +244,31 @@ class Activitypub { self::add_rewrite_rules(); \flush_rewrite_rules(); } + + public static function theme_compat() { + $site_icon = get_theme_support( 'custom-logo' ); + + if ( ! $site_icon ) { + // custom logo support + add_theme_support( + 'custom-logo', + array( + 'height' => 80, + 'width' => 80, + ) + ); + } + + $custom_header = get_theme_support( 'custom-header' ); + + if ( ! $custom_header ) { + // This theme supports a custom header + $custom_header_args = array( + 'width' => 1250, + 'height' => 600, + 'header-text' => true, + ); + add_theme_support( 'custom-header', $custom_header_args ); + } + } } diff --git a/templates/settings.php b/templates/settings.php index 08a65a1..5bc9c90 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -32,7 +32,25 @@

    -

    +

    + + + + + + + + +
    + + +
    + +
    +

    + +

    +
    From 913b60c7c7af445c65a4c033a9c42e29f0895f26 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 08:45:14 +0200 Subject: [PATCH 265/427] Fix WebFinger resources for Blog-User and updated settings. --- includes/class-activitypub.php | 5 +++++ includes/class-webfinger.php | 4 ++-- includes/model/class-blog-user.php | 2 +- includes/model/class-user.php | 4 ++++ templates/settings.php | 13 ++++++++++-- templates/welcome.php | 33 ++++++++++++++++++++++++++---- 6 files changed, 52 insertions(+), 9 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 7e5ea2f..cd95fc7 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -245,6 +245,11 @@ class Activitypub { \flush_rewrite_rules(); } + /** + * Theme compatibility stuff + * + * @return void + */ public static function theme_compat() { $site_icon = get_theme_support( 'custom-logo' ); diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 44a4a27..958f544 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -24,12 +24,12 @@ class Webfinger { return \get_webfinger_resource( $user_id, false ); } - $user = \get_user_by( 'id', $user_id ); + $user = User_Factory::get_by_id( $user_id ); if ( ! $user ) { return ''; } - return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $user->get_resource(); } /** diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 492bf76..d703275 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -24,7 +24,7 @@ class Blog_User extends User { * * @param int $user_id The User-ID. */ - public function __construct( $user_id ) { + public function __construct( $user_id = null ) { add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 2cee714..8791a50 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -247,4 +247,8 @@ class User { return $array; } + + public function get_resource() { + return $this->get_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + } } diff --git a/templates/settings.php b/templates/settings.php index 5bc9c90..6d8a0cb 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -42,10 +42,19 @@ diff --git a/templates/welcome.php b/templates/welcome.php index cb4d05f..acf986d 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -13,29 +13,54 @@

    +

    %1$s or the URL %2$s. Users who can not access this settings page will find their username on the Edit Profile page.', + 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings Page.', 'activitypub' ), - \esc_attr( \Activitypub\get_webfinger_resource( wp_get_current_user()->ID ) ), - \esc_url_raw( \get_author_posts_url( wp_get_current_user()->ID ) ), + \esc_attr( $blog_user->get_resource() ), + \esc_url_raw( $blog_user->get_url() ), + \esc_url_raw( \admin_url( '/options-general.php?page=activitypub&tab=settings' ) ) + ), + 'default' + ); + ?> +

    +

    +

    + ID ); + echo wp_kses( + \sprintf( + // translators: + \__( + 'People can also follow you by using your Username %1$s or your Author-URL %2$s. Users who can not access this settings page will find their username on the Edit Profile page.', + 'activitypub' + ), + \esc_attr( $user->get_resource() ), + \esc_url_raw( $user->get_url() ), \esc_url_raw( \admin_url( 'profile.php#activitypub' ) ) ), 'default' ); ?>

    +

    Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', 'activitypub' ), + \__( + 'If you have problems using this plugin, please check the Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', + 'activitypub' + ), \esc_url_raw( admin_url( 'site-health.php' ) ) ), 'default' From 6e237fe76cab78c5938efb118fb124b7d4d8c376 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 08:49:33 +0200 Subject: [PATCH 266/427] text changes --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index acf986d..a908d82 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -21,7 +21,7 @@ \sprintf( // translators: \__( - 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings Page.', + 'People can follow your Blog by using the username %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings page.', 'activitypub' ), \esc_attr( $blog_user->get_resource() ), From 0f72f944063e2b9690f5d315242d8bbc6ee41792 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 09:39:35 +0200 Subject: [PATCH 267/427] small updates --- includes/class-activity-dispatcher.php | 22 ++++++++++++++-------- templates/blog-json.php | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 811f923..bea6b4e 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -76,22 +76,28 @@ class Activity_Dispatcher { self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); } + /** + * Send Activities to followers and mentioned users. + * + * @param Post $activitypub_post The ActivityPub Post. + * @param string $activity_type The Activity-Type. + * @param int $user_id The User-ID. + * + * @return void + */ public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { if ( $user_id ) { - $user = User_Factory::get_by_id( $user_id ); - $actor = $user->get_url(); + $user = User_Factory::get_by_id( $user_id ); + $user_id = $user->get_id(); + $actor = $user->get_url(); } else { - // get latest version of post $user_id = $activitypub_post->get_post_author(); - $actor = null; + $actor = $activitypub_activity->get_actor(); } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - - if ( $actor ) { - $activitypub_activity->set_actor( $actor ); - } + $activitypub_activity->set_actor( $actor ); $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/templates/blog-json.php b/templates/blog-json.php index cfa759c..b9c5f96 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,5 +1,5 @@ Date: Wed, 31 May 2023 10:31:14 +0200 Subject: [PATCH 268/427] use correct blog-user-id --- includes/table/class-followers.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 5f1c199..3a107c7 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -2,6 +2,7 @@ namespace Activitypub\Table; use WP_List_Table; +use Activitypub\User_Factory; use Activitypub\Collection\Followers as FollowerCollection; if ( ! \class_exists( '\WP_List_Table' ) ) { @@ -13,7 +14,7 @@ class Followers extends WP_List_Table { public function __construct() { if ( get_current_screen()->id === 'settings_page_activitypub' ) { - $this->user_id = -1; + $this->user_id = User_Factory::BLOG_USER_ID; } else { $this->user_id = \get_current_user_id(); } From e1fd0e1c3977a95cabb9660d509ec972629db382 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 10:31:49 +0200 Subject: [PATCH 269/427] move signature to user object --- includes/class-signature.php | 25 +++++--------- includes/model/class-application-user.php | 40 +++++++++++++++++++++++ includes/model/class-blog-user.php | 39 +++++++++++++++++++++- includes/model/class-user.php | 36 +++++++++++++++++++- 4 files changed, 122 insertions(+), 18 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index a3293d6..66b16ba 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -5,6 +5,7 @@ use WP_Error; use DateTime; use DateTimeZone; use Activitypub\Model\User; +use Activitypub\User_Factory; /** * ActivityPub Signature Class @@ -73,7 +74,7 @@ class Signature { * * @return void */ - public static function generate_key_pair( $user_id ) { + public static function generate_key_pair() { $config = array( 'digest_alg' => 'sha512', 'private_key_bits' => 2048, @@ -84,22 +85,13 @@ class Signature { $priv_key = null; \openssl_pkey_export( $key, $priv_key ); + $detail = \openssl_pkey_get_details( $key ); - if ( User::APPLICATION_USER_ID === $user_id ) { - // private key - \update_option( 'activitypub_magic_sig_private_key', $priv_key ); - - // public key - \update_option( 'activitypub_magic_sig_public_key', $detail['key'] ); - - } else { - // private key - \update_user_meta( $user_id, 'magic_sig_private_key', $priv_key ); - - // public key - \update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] ); - } + return array( + 'private_key' => $priv_key, + 'public_key' => $detail['key'], + ); } /** @@ -114,7 +106,8 @@ class Signature { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { - $key = self::get_private_key( $user_id ); + $user = User_Factory::get_by_id( $user_id ); + $key = $user->get_private_key(); $url_parts = \wp_parse_url( $url ); diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 77ba7fb..1e562b1 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -40,4 +40,44 @@ class Application_User extends Blog_User { public function get_name() { return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); } + + public function get_public_key() { + $key = \get_option( 'activitypub_application_user_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + $key = \get_option( 'activitypub_application_user_public_key', true ); + + return $key; + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_option( 'activitypub_application_user_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_option( 'activitypub_application_user_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'], true ); + \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'], true ); + } + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index d703275..32b2dc7 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -2,6 +2,7 @@ namespace Activitypub\Model; use WP_Query; +use Activitypub\Signature; use Activitypub\User_Factory; class Blog_User extends User { @@ -141,6 +142,42 @@ class Blog_User extends User { } public function get_public_key() { - return ''; + $key = \get_option( 'activitypub_blog_user_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + $key = \get_option( 'activitypub_blog_user_public_key', true ); + + return $key; + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_option( 'activitypub_blog_user_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_option( 'activitypub_blog_user_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'], true ); + \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'], true ); + } } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 8791a50..302b784 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -130,7 +130,41 @@ class User { } public function get_public_key() { - return Signature::get_public_key( $this->user_id ); + $key = \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get_private_key() { + $key = \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + } + + private function generate_key_pair() { + $key_pair = Signature::generate_key_pair(); + + if ( ! is_wp_error( $key_pair ) ) { + \update_user_meta( $this->get_user_id(), 'magic_sig_public_key', $key_pair['public_key'], true ); + \update_user_meta( $this->get_user_id(), 'magic_sig_private_key', $key_pair['private_key'], true ); + } } /** From 4f2a162f6cfdf81a5e7b6c93043f3fd124e37ac3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 11:04:29 +0200 Subject: [PATCH 270/427] Fix follower-list actions --- includes/class-admin.php | 4 +-- templates/blog-user-followers-list.php | 30 +++++++++++++++++++ ...owers-list.php => user-followers-list.php} | 14 --------- 3 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 templates/blog-user-followers-list.php rename templates/{followers-list.php => user-followers-list.php} (73%) diff --git a/includes/class-admin.php b/includes/class-admin.php index b88a8de..1f8b640 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -65,7 +65,7 @@ class Admin { \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' ); break; case 'followers': - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' ); break; case 'welcome': default: @@ -85,7 +85,7 @@ class Admin { if ( ! current_user_can( 'publish_posts' ) ) { return; } - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/followers-list.php' ); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' ); } /** diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php new file mode 100644 index 0000000..db77e1e --- /dev/null +++ b/templates/blog-user-followers-list.php @@ -0,0 +1,30 @@ + '', + 'welcome' => '', + 'followers' => 'active', + ) +); +?> + +

    +

    + + + + +

    get_user_count() ) ); ?>

    + +
    + + + prepare_items(); + $followers_table->display(); + ?> + + +
    diff --git a/templates/followers-list.php b/templates/user-followers-list.php similarity index 73% rename from templates/followers-list.php rename to templates/user-followers-list.php index 733753e..7476192 100644 --- a/templates/followers-list.php +++ b/templates/user-followers-list.php @@ -1,17 +1,3 @@ -id === 'settings_page_activitypub' ) { - \load_template( - \dirname( __FILE__ ) . '/admin-header.php', - true, - array( - 'settings' => '', - 'welcome' => '', - 'followers' => 'active', - ) - ); -} -?> -

    From 73c767df399f4542d95e77023e7cec5619515137 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 13:47:37 +0200 Subject: [PATCH 271/427] fix admin-header issue --- templates/settings.php | 5 +++-- templates/welcome.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 6d8a0cb..477d3e6 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -3,8 +3,9 @@ \dirname( __FILE__ ) . '/admin-header.php', true, array( - 'settings' => 'active', - 'welcome' => '', + 'settings' => 'active', + 'welcome' => '', + 'followers' => '', ) ); ?> diff --git a/templates/welcome.php b/templates/welcome.php index a908d82..e3c47ee 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -3,8 +3,9 @@ \dirname( __FILE__ ) . '/admin-header.php', true, array( - 'settings' => '', - 'welcome' => 'active', + 'settings' => '', + 'welcome' => 'active', + 'followers' => '', ) ); ?> From e924019a7307f06f6aea3cf7a444c9236f5f2be7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 31 May 2023 13:51:23 +0200 Subject: [PATCH 272/427] added translators hint --- templates/blog-user-followers-list.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php index db77e1e..9ce3bd1 100644 --- a/templates/blog-user-followers-list.php +++ b/templates/blog-user-followers-list.php @@ -15,7 +15,7 @@ - +

    get_user_count() ) ); ?>

    From 112eb51af180c4d26a9a6fb86be1d7822924ab08 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:45:07 +0200 Subject: [PATCH 273/427] updated signature feature to new structure --- includes/class-signature.php | 2 +- includes/class-user-factory.php | 3 ++- includes/model/class-application-user.php | 13 +++++++--- includes/rest/class-server.php | 24 +++---------------- ...typub-rest-post-signature-verification.php | 8 +++++-- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index 66b16ba..a91ea0e 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -245,7 +245,7 @@ class Signature { * @return string The public key. */ public static function get_remote_key( $key_id ) { // phpcs:ignore - $actor = \Activitypub\get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore + $actor = get_remote_metadata_by_actor( strtok( strip_fragment_from_url( $key_id ), '?' ) ); // phpcs:ignore if ( \is_wp_error( $actor ) ) { return $actor; } diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 8ba4521..db3be67 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -5,6 +5,7 @@ use WP_Error; use WP_User_Query; use Activitypub\Model\User; use Activitypub\Model\Blog_User; +use Activitypub\Model\Application_User; class User_Factory { /** @@ -63,7 +64,7 @@ class User_Factory { } // check for application user. - if ( get_option( 'activitypub_application_user_identifier', null ) === $username ) { + if ( 'application' === $username ) { return self::get_by_id( self::APPLICATION_USER_ID ); } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 1e562b1..fe5fcc8 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -2,8 +2,11 @@ namespace Activitypub\Model; use WP_Query; +use Activitypub\Signature; use Activitypub\User_Factory; +use function Activitypub\get_rest_url_by_path; + class Application_User extends Blog_User { /** * The User-ID @@ -24,7 +27,7 @@ class Application_User extends Blog_User { * * @param int $user_id The User-ID. */ - public function __construct( $user_id ) { + public function __construct( $user_id = null ) { // do nothing } @@ -34,11 +37,15 @@ class Application_User extends Blog_User { * @return string The User-Url. */ public function get_url() { - return ''; + return get_rest_url_by_path( 'application' ); } public function get_name() { - return \esc_html( \get_option( 'activitypub_application_identifier', 'application' ) ); + return 'application'; + } + + public function get_username() { + return $this::get_name(); } public function get_public_key() { diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php index 351284d..3b78af2 100644 --- a/includes/rest/class-server.php +++ b/includes/rest/class-server.php @@ -4,11 +4,7 @@ namespace Activitypub\Rest; use stdClass; use WP_REST_Response; use Activitypub\Signature; -use Activitypub\Model\User; - -use function Activitypub\get_context; -use function Activitypub\get_rest_url_by_path; - +use Activitypub\Model\Application_User; /** * ActivityPub Server REST-Class @@ -18,7 +14,6 @@ use function Activitypub\get_rest_url_by_path; * @see https://www.w3.org/TR/activitypub/#security-verification */ class Server { - /** * Initialize the class, registering WordPress hooks */ @@ -50,21 +45,8 @@ class Server { * @return WP_REST_Response The JSON profile of the Application Actor. */ public static function application_actor() { - $json = new stdClass(); - - $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( 'application' ); - $json->type = 'Application'; - $json->preferredUsername = str_replace( array( '.' ), '-', wp_parse_url( get_site_url(), PHP_URL_HOST ) ); // phpcs:ignore WordPress.NamingConventions - $json->name = get_bloginfo( 'name' ); - $json->summary = __( 'WordPress-ActivityPub application actor', 'activitypub' ); - $json->manuallyApprovesFollowers = true; // phpcs:ignore WordPress.NamingConventions - $json->icon = array( get_site_icon_url() ); // phpcs:ignore WordPress.NamingConventions short array syntax - $json->publicKey = array( // phpcs:ignore WordPress.NamingConventions - 'id' => get_rest_url_by_path( 'application#main-key' ), - 'owner' => get_rest_url_by_path( 'application' ), - 'publicKeyPem' => Signature::get_public_key( User::APPLICATION_USER_ID ), // phpcs:ignore WordPress.NamingConventions - ); + $user = new Application_User(); + $json = $user->to_array(); $response = new WP_REST_Response( $json, 200 ); diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f0acd34..2b7fc29 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -42,7 +42,9 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); - $public_key = Activitypub\Signature::get_public_key( 1 ); + $user = Activitypub\User_Factory::get_by_id( 1 ); + + $public_key = $user->get_public_key(); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; @@ -53,6 +55,8 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { add_filter( 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { + $user = Activitypub\User_Factory::get_by_id( 1 ); + $public_key = $user->get_public_key(); // return ActivityPub Profile with signature return array( 'id' => $actor, @@ -60,7 +64,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { 'publicKey' => array( 'id' => $actor . '#main-key', 'owner' => $actor, - 'publicKeyPem' => \Activitypub\Signature::get_public_key( 1 ), + 'publicKeyPem' => $public_key, ), ); }, From d251060624c466703185b9f901aa3e87f357ea4e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 11:50:01 +0200 Subject: [PATCH 274/427] migrated missing parts --- includes/class-http.php | 4 ++-- includes/class-signature.php | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index 58551a4..240e5ea 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,7 +2,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\Model\User; +use Activitypub\User_Factory; /** * ActivityPub HTTP Class @@ -63,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( User::APPLICATION_USER_ID, 'get', $url, $date ); + $signature = Signature::generate_signature( User_Factory::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-signature.php b/includes/class-signature.php index a91ea0e..c1ccbdf 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -136,11 +136,8 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - if ( User::APPLICATION_USER_ID === $user_id ) { - $key_id = \get_rest_url( null, 'activitypub/1.0/application#main-key' ); - } else { - $key_id = \get_author_posts_url( $user_id ) . '#main-key'; - } + $user = User_Factory::get_by_id( $user_id ); + $key_id = $user->get_url() . '#main-key'; if ( ! empty( $digest ) ) { return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature ); From 723a3e336324ee3cdc7b5d8f4692365e64fea144 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 1 Jun 2023 12:47:08 +0200 Subject: [PATCH 275/427] fix signature issue --- includes/model/class-application-user.php | 12 ++++++------ includes/model/class-blog-user.php | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index fe5fcc8..bbeedd0 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -49,7 +49,7 @@ class Application_User extends Blog_User { } public function get_public_key() { - $key = \get_option( 'activitypub_application_user_public_key', true ); + $key = \get_option( 'activitypub_application_user_public_key' ); if ( $key ) { return $key; @@ -57,7 +57,7 @@ class Application_User extends Blog_User { $this->generate_key_pair(); - $key = \get_option( 'activitypub_application_user_public_key', true ); + $key = \get_option( 'activitypub_application_user_public_key' ); return $key; } @@ -68,7 +68,7 @@ class Application_User extends Blog_User { * @return mixed */ public function get_private_key() { - $key = \get_option( 'activitypub_application_user_private_key', true ); + $key = \get_option( 'activitypub_application_user_private_key' ); if ( $key ) { return $key; @@ -76,15 +76,15 @@ class Application_User extends Blog_User { $this->generate_key_pair(); - return \get_option( 'activitypub_application_user_private_key', true ); + return \get_option( 'activitypub_application_user_private_key' ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'], true ); - \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'], true ); + \update_option( 'activitypub_application_user_public_key', $key_pair['public_key'] ); + \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'] ); } } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 32b2dc7..c92fdd3 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -142,7 +142,7 @@ class Blog_User extends User { } public function get_public_key() { - $key = \get_option( 'activitypub_blog_user_public_key', true ); + $key = \get_option( 'activitypub_blog_user_public_key' ); if ( $key ) { return $key; @@ -150,7 +150,7 @@ class Blog_User extends User { $this->generate_key_pair(); - $key = \get_option( 'activitypub_blog_user_public_key', true ); + $key = \get_option( 'activitypub_blog_user_public_key' ); return $key; } @@ -161,7 +161,7 @@ class Blog_User extends User { * @return mixed */ public function get_private_key() { - $key = \get_option( 'activitypub_blog_user_private_key', true ); + $key = \get_option( 'activitypub_blog_user_private_key' ); if ( $key ) { return $key; @@ -169,15 +169,15 @@ class Blog_User extends User { $this->generate_key_pair(); - return \get_option( 'activitypub_blog_user_private_key', true ); + return \get_option( 'activitypub_blog_user_private_key' ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'], true ); - \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'], true ); + \update_option( 'activitypub_blog_user_public_key', $key_pair['public_key'] ); + \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); } } } From a8fe587f913c52fba41154bb92640daf2fbc67eb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 14 Jun 2023 15:02:45 +0200 Subject: [PATCH 276/427] prepare federation method --- includes/class-activity-dispatcher.php | 16 +++++++------- includes/class-scheduler.php | 29 ++++++++++++++++---------- includes/functions.php | 3 ++- includes/model/class-blog-user.php | 7 ++++++- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index bea6b4e..cde9785 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -26,6 +26,8 @@ class Activity_Dispatcher { \add_action( 'activitypub_send_create_activity', array( self::class, 'send_create_activity' ) ); \add_action( 'activitypub_send_update_activity', array( self::class, 'send_update_activity' ) ); \add_action( 'activitypub_send_delete_activity', array( self::class, 'send_delete_activity' ) ); + + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); } /** @@ -67,13 +69,8 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - if ( ! is_single_user_mode() ) { - // send User-Activity - self::send_user_activity( $activitypub_post, $activity_type ); - } - // send Blog-User-Activity - self::send_user_activity( $activitypub_post, $activity_type, User_Factory::BLOG_USER_ID ); + self::send_user_activity( $activitypub_post, $activity_type ); } /** @@ -92,12 +89,15 @@ class Activity_Dispatcher { $actor = $user->get_url(); } else { $user_id = $activitypub_post->get_post_author(); - $actor = $activitypub_activity->get_actor(); + $actor = null; } $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - $activitypub_activity->set_actor( $actor ); + + if ( $actor ) { + $activitypub_activity->set_actor( $actor ); + } $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index d55fb35..a5cfbc8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -70,22 +70,29 @@ class Scheduler { $activitypub_post = new Post( $post ); + $activity_type = false; + if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_create_activity', - array( $activitypub_post ) - ); + $activity_type = 'Create'; } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_activity', - array( $activitypub_post ) - ); + $activity_type = 'Update'; } elseif ( 'trash' === $new_status ) { + $activity_type = 'Delete'; + } + + if ( $activity_type ) { \wp_schedule_single_event( \time(), - 'activitypub_send_delete_activity', + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), array( $activitypub_post ) ); } diff --git a/includes/functions.php b/includes/functions.php index dfbdf37..595e85b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -56,9 +56,10 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { return $actor; } + $transient_key = 'activitypub_' . $actor; + // only check the cache if needed. if ( $cached ) { - $transient_key = 'activitypub_' . $actor; $metadata = \get_transient( $transient_key ); if ( $metadata ) { diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c92fdd3..8a04018 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -44,7 +44,12 @@ class Blog_User extends User { * @return string The User-Description. */ public function get_summary() { - return \wpautop( \wp_kses( \get_bloginfo( 'description' ), 'default' ) ); + return \wpautop( + \wp_kses( + \get_bloginfo( 'description' ), + 'default' + ) + ); } /** From 255ace3ae6ac8c0915ae652eb2de1d1494141c11 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:44:46 +0200 Subject: [PATCH 277/427] revert latest changes to simplify dispatching for now --- includes/class-activity-dispatcher.php | 30 ++------------------------ 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index cde9785..00ffd48 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -69,37 +69,11 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - // send Blog-User-Activity - self::send_user_activity( $activitypub_post, $activity_type ); - } - - /** - * Send Activities to followers and mentioned users. - * - * @param Post $activitypub_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. - * @param int $user_id The User-ID. - * - * @return void - */ - public static function send_user_activity( Post $activitypub_post, $activity_type, $user_id = null ) { - if ( $user_id ) { - $user = User_Factory::get_by_id( $user_id ); - $user_id = $user->get_id(); - $actor = $user->get_url(); - } else { - $user_id = $activitypub_post->get_post_author(); - $actor = null; - } - $activitypub_activity = new Activity( $activity_type ); $activitypub_activity->from_post( $activitypub_post ); - if ( $actor ) { - $activitypub_activity->set_actor( $actor ); - } - - $follower_inboxes = Followers::get_inboxes( $user_id ); + $user_id = $activitypub_post->get_user_id(); + $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); From 5f1abd246159f5404d62f683c3b7364d3de020a6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:45:03 +0200 Subject: [PATCH 278/427] fail early --- includes/class-scheduler.php | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index a5cfbc8..293b610 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -80,22 +80,24 @@ class Scheduler { $activity_type = 'Delete'; } - if ( $activity_type ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $activity_type ) - ), - array( $activitypub_post ) - ); + if ( ! $activity_type ) { + return; } + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); } /** From e88ee5911376dc61a6dfb7c47d4a09819f05d228 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:45:35 +0200 Subject: [PATCH 279/427] make user filterable, to change author to blog wide user --- includes/model/class-post.php | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 8e58a07..5ebeb34 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -1,6 +1,7 @@ post_author, $this->post ); + } + /** * Converts this Object into an Array. * - * @return array + * @return array the array representation of a Post. */ public function to_array() { $post = $this->post; @@ -203,7 +213,7 @@ class Post { 'url' => $this->get_url(), 'type' => $this->get_object_type(), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), - 'attributedTo' => \get_author_posts_url( $post->post_author ), + 'attributedTo' => $this->get_actor(), 'summary' => $this->get_summary(), 'inReplyTo' => null, 'content' => $this->get_content(), @@ -219,6 +229,17 @@ class Post { return \apply_filters( 'activitypub_post', $array, $this->post ); } + /** + * Returns the Actor of this Object. + * + * @return string The URL of the Actor. + */ + public function get_actor() { + $user = User_Factory::get_by_id( $this->get_user_id() ); + + return $user->get_url(); + } + /** * Converts this Object into a JSON String * From 6ddbe25852400535c0e9e3ff80d95c0e0b3383b6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:46:25 +0200 Subject: [PATCH 280/427] overwrite activity-object-user on single_user_mode --- includes/class-activitypub.php | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cd95fc7..733ae00 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -30,6 +30,15 @@ class Activitypub { \add_action( 'init', array( self::class, 'add_rewrite_rules' ) ); \add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 ); + + if ( is_single_user_mode() ) { + add_filter( + 'activitypub_post_user_id', + function( $actor ) { + return User_Factory::BLOG_USER_ID; + } + ); + } } /** @@ -71,12 +80,15 @@ class Activitypub { * @return string The new path to the JSON template. */ public static function render_json_template( $template ) { - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + if ( ! is_activitypub_request() ) { return $template; } - // Ensure that edge caches know that this page can deliver both HTML and JSON. - header( 'Vary: Accept' ); + $json_template = false; + + if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { + return $template; + } // check if user can publish posts if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { @@ -91,15 +103,12 @@ class Activitypub { $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php'; } - if ( is_activitypub_request() ) { - if ( ACTIVITYPUB_SECURE_MODE ) { - $verification = Signature::verify_http_signature( $_SERVER ); - if ( \is_wp_error( $verification ) ) { - // fallback as template_loader can't return http headers - return $template; - } + if ( ACTIVITYPUB_SECURE_MODE ) { + $verification = Signature::verify_http_signature( $_SERVER ); + if ( \is_wp_error( $verification ) ) { + // fallback as template_loader can't return http headers + return $template; } - return $json_template; } From 359cd5708161cffcf52f397419e21b75d27f3caf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 15:46:34 +0200 Subject: [PATCH 281/427] normalizing --- includes/class-user-factory.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index db3be67..ce70ccb 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -130,8 +130,8 @@ class User_Factory { $resource = \str_replace( 'acct:', '', $resource ); $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) ); - $resource_host = \str_replace( 'www.', '', \substr( \strrchr( $resource, '@' ), 1 ) ); - $blog_host = \str_replace( 'www.', '', \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + $resource_host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) ); + $blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); if ( $blog_host !== $resource_host ) { return new WP_Error( @@ -160,4 +160,15 @@ class User_Factory { return self::get_by_username( $id ); } } + + /** + * Normalize the host. + * + * @param string $host The host. + * + * @return string The normalized host. + */ + public static function normalize_host( $host ) { + return \str_replace( 'www.', '', $host ); + } } From 58c04856c9a9efbc7574249b144142bf67ed7ee9 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 21 Jun 2023 17:10:52 +0200 Subject: [PATCH 282/427] check if a user is enabled or not --- includes/class-user-factory.php | 42 ++++++++++++++++++------------ includes/functions.php | 46 +++++++++++++++++++++++++++++++++ includes/model/class-post.php | 2 +- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index ce70ccb..4a0ce5a 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -30,24 +30,32 @@ class User_Factory { * @return \Acitvitypub\Model\User The User. */ public static function get_by_id( $user_id ) { - $user_id = (int) $user_id; - - if ( self::BLOG_USER_ID === $user_id ) { - return new Blog_User( $user_id ); - } elseif ( self::APPLICATION_USER_ID === $user_id ) { - return new Application_User( $user_id ); - } else { - $user = get_user_by( 'ID', $user_id ); - if ( ! $user || ! \user_can( $user, 'publish_posts' ) ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - return new User( $user->ID ); + if ( is_string( $user_id ) || is_numeric( $user_id ) ) { + $user_id = (int) $user_id; } + + if ( + self::BLOG_USER_ID === $user_id && + is_user_enabled( $user_id ) + ) { + return new Blog_User( $user_id ); + } elseif ( + self::APPLICATION_USER_ID === $user_id && + is_user_enabled( $user_id ) + ) { + return new Application_User( $user_id ); + } elseif ( + $user_id > 0 && + is_user_enabled( $user_id ) + ) { + return new User( $user_id ); + } + + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); } /** diff --git a/includes/functions.php b/includes/functions.php index 595e85b..7fc62d8 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -277,3 +277,49 @@ function is_activitypub_request() { function is_single_user_mode() { return ACTIVITYPUB_SINGLE_USER_MODE; } + +/** + * This function checks if a user is enabled for ActivityPub. + * + * @param int $user_id The User-ID. + * + * @return boolean True if the user is enabled, false otherwise. + */ +function is_user_enabled( $user_id ) { + switch ( $user_id ) { + // if the user is the application user, it's always enabled. + case \Activitypub\User_Factory::APPLICATION_USER_ID: + return true; + // if the user is the blog user, it's only enabled in single-user mode. + case \Activitypub\User_Factory::BLOG_USER_ID: + if ( is_single_user_mode() ) { + return true; + } + + return false; + // if the user is any other user, it's enabled if it can publish posts. + default: + if ( + ! is_single_user_mode() && + \user_can( $user_id, 'publish_posts' ) + ) { + return true; + } + + return false; + } +} + +if ( ! function_exists( 'get_self_link' ) ) { + /** + * Get the correct self URL + * + * @return boolean + */ + function get_self_link() { + $host = wp_parse_url( home_url() ); + + return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); + } +} + diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 5ebeb34..5004162 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -197,7 +197,7 @@ class Post { * @return int the User ID. */ public function get_user_id() { - return apply_filters( 'activitypub_post_user_id', $this->post_author, $this->post ); + return apply_filters( 'activitypub_post_user_id', $this->get_post_author(), $this->post ); } /** From 36540c0f78c40904fc7004817ec893e032477622 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 09:56:18 +0200 Subject: [PATCH 283/427] fix delete --- includes/table/class-followers.php | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 3a107c7..f16a479 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -34,7 +34,7 @@ class Followers extends WP_List_Table { 'avatar' => \__( 'Avatar', 'activitypub' ), 'name' => \__( 'Name', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), - 'identifier' => \__( 'Identifier', 'activitypub' ), + 'url' => \__( 'URL', 'activitypub' ), 'updated' => \__( 'Last updated', 'activitypub' ), //'errors' => \__( 'Errors', 'activitypub' ), //'latest-error' => \__( 'Latest Error Message', 'activitypub' ), @@ -55,8 +55,8 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $follower = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); - $counter = FollowerCollection::count_followers( $this->user_id ); + $followers = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); $this->set_pagination_args( @@ -72,7 +72,8 @@ class Followers extends WP_List_Table { 'icon' => esc_attr( $follower->get_icon_url() ), 'name' => esc_attr( $follower->get_name() ), 'username' => esc_attr( $follower->get_preferred_username() ), - 'identifier' => esc_attr( $follower->get_url() ), + 'url' => esc_attr( $follower->get_url() ), + 'identifier' => esc_attr( $follower->get_id() ), 'updated' => esc_attr( $follower->get_updated() ), 'errors' => $follower->count_errors(), 'latest-error' => $follower->get_latest_error_message(), @@ -102,11 +103,11 @@ class Followers extends WP_List_Table { ); } - public function column_identifier( $item ) { + public function column_url( $item ) { return sprintf( '%s', - $item['identifier'], - $item['identifier'] + $item['url'], + $item['url'] ); } @@ -135,7 +136,7 @@ class Followers extends WP_List_Table { $followers = array( $followers ); } foreach ( $followers as $follower ) { - FollowerCollection::remove_follower( \get_current_user_id(), $follower ); + FollowerCollection::remove_follower( $this->user_id, $follower ); } break; } From 83ddca8f28b3b37972175c5c601c1f2f9d4868b6 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 10:14:13 +0200 Subject: [PATCH 284/427] fix templating --- includes/class-activitypub.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 733ae00..a4b29d0 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -109,10 +109,9 @@ class Activitypub { // fallback as template_loader can't return http headers return $template; } - return $json_template; } - return $template; + return $json_template; } /** From c266c927da203167de8eee21f688e80fdff75636 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 14:22:27 +0200 Subject: [PATCH 285/427] transform users to actors --- activitypub.php | 4 +- includes/activity/class-actor.php | 14 +++ includes/activity/class-base-object.php | 10 +- includes/activity/class-person.php | 20 ---- includes/class-activitypub.php | 11 +- includes/class-admin.php | 16 +-- includes/class-user-factory.php | 21 +--- includes/functions.php | 30 ++--- includes/model/class-application-user.php | 13 +-- includes/model/class-blog-user.php | 12 +- includes/model/class-user.php | 128 ++++++++-------------- includes/rest/class-followers.php | 6 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 6 +- includes/rest/class-outbox.php | 4 +- templates/admin-header.php | 4 + templates/author-json.php | 6 +- templates/blog-json.php | 6 +- templates/settings.php | 31 +----- templates/welcome.php | 11 ++ 20 files changed, 134 insertions(+), 221 deletions(-) delete mode 100644 includes/activity/class-person.php diff --git a/activitypub.php b/activitypub.php index 7bc4306..75d52b1 100644 --- a/activitypub.php +++ b/activitypub.php @@ -28,7 +28,9 @@ function init() { \defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); - \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) || \define( 'ACTIVITYPUB_SINGLE_USER_MODE', false ); + + \defined( 'ACTIVITYPUB_DISABLE_USER' ) || \define( 'ACTIVITYPUB_DISABLE_USER', false ); + \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) || \define( 'ACTIVITYPUB_DISABLE_BLOG_USER', false ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 604261f..202783f 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -114,4 +114,18 @@ class Actor extends Base_Object { * @var string|array|null */ protected $public_key; + + /** + * It's not part of the ActivityPub protocol but it's a quite common + * practice to lock an account. If anabled, new followers will not be + * automatically accepted, but will instead require you to manually + * approve them. + * + * WordPress does only support 'false' at the moment. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#as + * + * @var boolean + */ + protected $manually_approves_followers = false; } diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 009cab3..18382a5 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -577,16 +577,22 @@ class Base_Object { foreach ( $vars as $key => $value ) { // if value is empty, try to get it from a getter. - if ( ! $value ) { + if ( ! isset( $value ) ) { $value = call_user_func( array( $this, 'get_' . $key ) ); } // if value is still empty, ignore it for the array and continue. - if ( $value ) { + if ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; } } + $class = new \ReflectionClass( $this ); + $class = strtolower( $class->getShortName() ); + + $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); + $array = \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); + return $array; } } diff --git a/includes/activity/class-person.php b/includes/activity/class-person.php deleted file mode 100644 index f2ed9b9..0000000 --- a/includes/activity/class-person.php +++ /dev/null @@ -1,20 +0,0 @@ - 0 && - is_user_enabled( $user_id ) - ) { - return new User( $user_id ); + if ( self::BLOG_USER_ID === $user_id ) { + return Blog_User::from_wp_user( $user_id ); + } elseif ( self::APPLICATION_USER_ID === $user_id ) { + return Application_User::from_wp_user( $user_id ); + } elseif ( $user_id > 0 ) { + return User::from_wp_user( $user_id ); } return new WP_Error( diff --git a/includes/functions.php b/includes/functions.php index 7fc62d8..a66d44e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -270,42 +270,34 @@ function is_activitypub_request() { } /** - * Check if the current site is in single-user mode. - * - * @return boolean - */ -function is_single_user_mode() { - return ACTIVITYPUB_SINGLE_USER_MODE; -} - -/** - * This function checks if a user is enabled for ActivityPub. + * This function checks if a user is disabled for ActivityPub. * * @param int $user_id The User-ID. * - * @return boolean True if the user is enabled, false otherwise. + * @return boolean True if the user is disabled, false otherwise. */ -function is_user_enabled( $user_id ) { +function is_user_disabled( $user_id ) { switch ( $user_id ) { // if the user is the application user, it's always enabled. case \Activitypub\User_Factory::APPLICATION_USER_ID: - return true; + return false; // if the user is the blog user, it's only enabled in single-user mode. case \Activitypub\User_Factory::BLOG_USER_ID: - if ( is_single_user_mode() ) { - return true; + if ( defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) { + return ACTIVITYPUB_DISABLE_BLOG_USER; } return false; // if the user is any other user, it's enabled if it can publish posts. default: - if ( - ! is_single_user_mode() && - \user_can( $user_id, 'publish_posts' ) - ) { + if ( ! \user_can( $user_id, 'publish_posts' ) ) { return true; } + if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { + return ACTIVITYPUB_DISABLE_USER; + } + return false; } } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index bbeedd0..d9bee08 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -13,23 +13,14 @@ class Application_User extends Blog_User { * * @var int */ - public $user_id = User_Factory::APPLICATION_USER_ID; + protected $_id = User_Factory::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Application'; - - /** - * The User constructor. - * - * @param int $user_id The User-ID. - */ - public function __construct( $user_id = null ) { - // do nothing - } + protected $type = 'Application'; /** * Get the User-Url. diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 8a04018..fe15bd9 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -11,22 +11,22 @@ class Blog_User extends User { * * @var int */ - public $user_id = User_Factory::BLOG_USER_ID; + protected $_id = User_Factory::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Person'; + protected $type = 'Person'; /** * The User constructor. * * @param int $user_id The User-ID. */ - public function __construct( $user_id = null ) { - add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + public function __construct() { + add_filter( 'activitypub_activity_blog_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); } /** @@ -61,10 +61,6 @@ class Blog_User extends User { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } - public function get_canonical_url() { - return \get_home_url(); - } - /** * Generate and save a default Username. * diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 302b784..320a713 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -6,34 +6,45 @@ use WP_Error; use Activitypub\Signature; use Activitypub\Model\User; use Activitypub\User_Factory; +use Activitypub\Activity\Actor; +use function Activitypub\is_user_disabled; use function Activitypub\get_rest_url_by_path; -class User { +class User extends Actor { /** * The User-ID * * @var int */ - public $user_id; + protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * The User-Type * * @var string */ - private $type = 'Person'; + protected $type = 'Person'; /** * The User constructor. * * @param numeric $user_id The User-ID. */ - public function __construct( $user_id ) { - $this->user_id = $user_id; + public function __construct() { + add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_attachments' ), 10, 2 ); + } - add_filter( 'activitypub_json_author_array', array( $this, 'add_api_endpoints' ), 10, 2 ); - add_filter( 'activitypub_json_author_array', array( $this, 'add_attachments' ), 10, 2 ); + public static function from_wp_user( $user_id ) { + if ( is_user_disabled( $user_id ) ) { + return null; + } + + $object = new static(); + $object->_id = $user_id; + + return $object; } /** @@ -71,7 +82,7 @@ class User { * @return string The User-Name. */ public function get_name() { - return \esc_attr( \get_the_author_meta( 'display_name', $this->user_id ) ); + return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); } /** @@ -80,9 +91,9 @@ class User { * @return string The User-Description. */ public function get_summary() { - $description = get_user_meta( $this->user_id, 'activitypub_user_description', true ); + $description = get_user_meta( $this->_id, 'activitypub_user_description', true ); if ( empty( $description ) ) { - $description = get_user_meta( $this->user_id, 'description', true ); + $description = get_user_meta( $this->_id, 'description', true ); } return \wpautop( \wp_kses( $description, 'default' ) ); } @@ -93,44 +104,49 @@ class User { * @return string The User-Url. */ public function get_url() { - return \esc_url( \get_author_posts_url( $this->user_id ) ); - } - - public function get_canonical_url() { - return $this->get_url(); + return \esc_url( \get_author_posts_url( $this->_id ) ); } public function get_at_url() { return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); } - public function get_username() { - return \esc_attr( \get_the_author_meta( 'login', $this->user_id ) ); + public function get_preferred_username() { + return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); } - public function get_avatar() { - return \esc_url( + public function get_icon() { + $icon = \esc_url( \get_avatar_url( - $this->user_id, + $this->_id, array( 'size' => 120 ) ) ); + + return array( + 'type' => 'Image', + 'url' => $icon, + ); } - public function get_header_image() { + public function get_image() { if ( \has_header_image() ) { - return \esc_url( \get_header_image() ); + $image = \esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => $image, + ); } return null; } public function get_published() { - return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->user_id ) ) ); + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } public function get_public_key() { - $key = \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + $key = \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); if ( $key ) { return $key; @@ -138,7 +154,7 @@ class User { $this->generate_key_pair(); - return \get_user_meta( $this->get_user_id(), 'magic_sig_public_key', true ); + return \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); } /** @@ -147,7 +163,7 @@ class User { * @return mixed */ public function get_private_key() { - $key = \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); if ( $key ) { return $key; @@ -155,70 +171,18 @@ class User { $this->generate_key_pair(); - return \get_user_meta( $this->get_user_id(), 'magic_sig_private_key', true ); + return \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); } private function generate_key_pair() { $key_pair = Signature::generate_key_pair(); if ( ! is_wp_error( $key_pair ) ) { - \update_user_meta( $this->get_user_id(), 'magic_sig_public_key', $key_pair['public_key'], true ); - \update_user_meta( $this->get_user_id(), 'magic_sig_private_key', $key_pair['private_key'], true ); + \update_user_meta( $this->get__id(), 'magic_sig_public_key', $key_pair['public_key'], true ); + \update_user_meta( $this->get__id(), 'magic_sig_private_key', $key_pair['private_key'], true ); } } - /** - * Array representation of the User. - * - * @param bool $context Whether to include the @context. - * - * @return array The array representation of the User. - */ - public function to_array( $context = true ) { - $output = array(); - - if ( $context ) { - $output['@context'] = Activity::CONTEXT; - } - - $output['id'] = $this->get_url(); - $output['type'] = $this->get_type(); - $output['name'] = $this->get_name(); - $output['summary'] = \html_entity_decode( - $this->get_summary(), - \ENT_QUOTES, - 'UTF-8' - ); - $output['preferredUsername'] = $this->get_username(); // phpcs:ignore - $output['url'] = $this->get_url(); - $output['icon'] = array( - 'type' => 'Image', - 'url' => $this->get_avatar(), - ); - - if ( $this->has_header_image() ) { - $output['image'] = array( - 'type' => 'Image', - 'url' => $this->get_header_image(), - ); - } - - $output['published'] = $this->get_published(); - - $output['publicKey'] = array( - 'id' => $this->get_url() . '#main-key', - 'owner' => $this->get_url(), - 'publicKeyPem' => \trim( $this->get_public_key() ), - ); - - $output['manuallyApprovesFollowers'] = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore - - // filter output - $output = \apply_filters( 'activitypub_json_author_array', $output, $this->user_id, $this ); - - return $output; - } - /** * Extend the User-Output with API-Endpoints. * @@ -283,6 +247,6 @@ class User { } public function get_resource() { - return $this->get_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 792cdfb..477393e 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -72,15 +72,15 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore - $json->totalItems = FollowerCollection::count_followers( $user->get_user_id() ); // phpcs:ignore + $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get_user_id() ) + FollowerCollection::get_followers( $user->get__id() ) ); $response = new WP_REST_Response( $json, 200 ); diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 0d1de61..6f13482 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -67,7 +67,7 @@ class Following { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 54e2894..b8d75c9 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -93,7 +93,7 @@ class Inbox { $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore @@ -136,8 +136,8 @@ class Inbox { $type = $request->get_param( 'type' ); $type = \strtolower( $type ); - \do_action( 'activitypub_inbox', $data, $user->get_user_id(), $type ); - \do_action( "activitypub_inbox_{$type}", $data, $user->get_user_id() ); + \do_action( 'activitypub_inbox', $data, $user->get__id(), $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() ); return new WP_REST_Response( array(), 202 ); } diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index cb09cfd..1dc05c7 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -75,7 +75,7 @@ class Outbox { $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get_user_id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -97,7 +97,7 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user->get_user_id(), + 'author' => $user->get__id(), 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) diff --git a/templates/admin-header.php b/templates/admin-header.php index 974b224..f2e7ed5 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -12,9 +12,13 @@ + + + +

    diff --git a/templates/author-json.php b/templates/author-json.php index aa38b97..fe569db 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -4,7 +4,7 @@ $user = \Activitypub\User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); +\do_action( 'activitypub_json_author_pre', $user->get__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -19,7 +19,7 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get__id() ); \header( 'Content-Type: application/activity+json' ); echo \wp_json_encode( $user->to_array(), $options ); @@ -27,4 +27,4 @@ echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $user->get_user_id() ); +\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/templates/blog-json.php b/templates/blog-json.php index b9c5f96..677c574 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -4,7 +4,7 @@ $user = new \Activitypub\Model\Blog_User(); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ -\do_action( 'activitypub_json_author_pre', $user->get_user_id() ); +\do_action( 'activitypub_json_author_pre', $user->get__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -19,7 +19,7 @@ $options |= \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; * * @param int $options The current options flags */ -$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get_user_id() ); +$options = \apply_filters( 'activitypub_json_author_options', $options, $user->get__id() ); \header( 'Content-Type: application/activity+json' ); echo \wp_json_encode( $user->to_array(), $options ); @@ -27,4 +27,4 @@ echo \wp_json_encode( $user->to_array(), $options ); /* * Action triggerd after the ActivityPub profile has been created and sent to the client */ -\do_action( 'activitypub_json_author_post', $user->get_user_id() ); +\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/templates/settings.php b/templates/settings.php index 477d3e6..162ea02 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,37 +31,12 @@ + +

    -
    - +

    - + Customizer.', 'activitypub' ), + \esc_url_raw( \admin_url( 'customize.php' ) ) + ), + 'default' + ); + ?>

    - - - - - - -
    - - -
    - -
    -

    - Customizer.', 'activitypub' ), - \esc_url_raw( \admin_url( 'customize.php' ) ) - ), - 'default' - ); - ?> -

    -
    - @@ -83,6 +58,8 @@ + +

    diff --git a/templates/welcome.php b/templates/welcome.php index e3c47ee..0bce61c 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -14,6 +14,9 @@

    + + +

    + + + + +

    + + +

    Date: Wed, 28 Jun 2023 16:42:20 +0200 Subject: [PATCH 286/427] put @context at the top of the JSON output --- includes/activity/class-base-object.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 18382a5..0ad4457 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -587,6 +587,13 @@ class Base_Object { } } + // replace 'context' key with '@context' and move it to the top. + if ( array_key_exists( 'context', $array ) ) { + $context = $array['context']; + unset( $array['context'] ); + $array = array_merge( array( '@context' => $context ), $array ); + } + $class = new \ReflectionClass( $this ); $class = strtolower( $class->getShortName() ); From a706bef1306c6af78d748f5a4c814a2ac350e32a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:42:33 +0200 Subject: [PATCH 287/427] check for option field --- includes/class-user-factory.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/class-user-factory.php b/includes/class-user-factory.php index 8011445..8cc9f26 100644 --- a/includes/class-user-factory.php +++ b/includes/class-user-factory.php @@ -62,6 +62,10 @@ class User_Factory { return self::get_by_id( self::BLOG_USER_ID ); } + if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + // check for application user. if ( 'application' === $username ) { return self::get_by_id( self::APPLICATION_USER_ID ); From 43db2f27078feca8f421b4c0ca144283e2121050 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:43:05 +0200 Subject: [PATCH 288/427] set context for output --- templates/author-json.php | 4 ++++ templates/blog-json.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/templates/author-json.php b/templates/author-json.php index fe569db..3defd98 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,6 +1,10 @@ set_context( + \Activitypub\Model\Activity::CONTEXT +); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ diff --git a/templates/blog-json.php b/templates/blog-json.php index 677c574..635e7d5 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,6 +1,10 @@ set_context( + \Activitypub\Model\Activity::CONTEXT +); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ From c02702f773bf7d445c75c5a35691e1074dd94f40 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 16:43:41 +0200 Subject: [PATCH 289/427] replace filters --- includes/model/class-application-user.php | 20 +++++ includes/model/class-blog-user.php | 26 ++++-- includes/model/class-user.php | 103 ++++++++++------------ 3 files changed, 87 insertions(+), 62 deletions(-) diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index d9bee08..9c53f9d 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -78,4 +78,24 @@ class Application_User extends Blog_User { \update_option( 'activitypub_application_user_private_key', $key_pair['private_key'] ); } } + + public function get_inbox() { + return null; + } + + public function get_outbox() { + return null; + } + + public function get_followers() { + return null; + } + + public function get_following() { + return null; + } + + public function get_attachment() { + return array(); + } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index fe15bd9..a4d3374 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -5,6 +5,8 @@ use WP_Query; use Activitypub\Signature; use Activitypub\User_Factory; +use function Activitypub\is_user_disabled; + class Blog_User extends User { /** * The User-ID @@ -20,13 +22,19 @@ class Blog_User extends User { */ protected $type = 'Person'; - /** - * The User constructor. - * - * @param int $user_id The User-ID. - */ - public function __construct() { - add_filter( 'activitypub_activity_blog_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); + public static function from_wp_user( $user_id ) { + if ( is_user_disabled( $user_id ) ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $object = new static(); + $object->_id = $user_id; + + return $object; } /** @@ -181,4 +189,8 @@ class Blog_User extends User { \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); } } + + public function get_attachment() { + return array(); + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 320a713..2ee2258 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -26,19 +26,16 @@ class User extends Actor { */ protected $type = 'Person'; - /** - * The User constructor. - * - * @param numeric $user_id The User-ID. - */ - public function __construct() { - add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_api_endpoints' ), 10, 2 ); - add_filter( 'activitypub_activity_user_object_array', array( $this, 'add_attachments' ), 10, 2 ); - } - public static function from_wp_user( $user_id ) { - if ( is_user_disabled( $user_id ) ) { - return null; + if ( + is_user_disabled( $user_id ) || + ! get_user_by( 'id', $user_id ) + ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); } $object = new static(); @@ -47,26 +44,6 @@ class User extends Actor { return $object; } - /** - * Magic function to implement getter and setter - * - * @param string $method - * @param string $params - * - * @return void - */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - return $this->$var; - } - - if ( \strncasecmp( $method, 'has', 3 ) === 0 ) { - return (bool) call_user_func( 'get_' . $var, $this ); - } - } - /** * Get the User-ID. * @@ -158,7 +135,7 @@ class User extends Actor { } /** - * @param int $user_id + * @param int $this->get__id() * * @return mixed */ @@ -184,34 +161,50 @@ class User extends Actor { } /** - * Extend the User-Output with API-Endpoints. + * Returns the Inbox-API-Endpoint. * - * @param array $array The User-Output. - * @param numeric $user_id The User-ID. - * - * @return array The extended User-Output. + * @return string The Inbox-Endpoint. */ - public function add_api_endpoints( $array, $user_id ) { - $array['inbox'] = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); - $array['outbox'] = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); - $array['followers'] = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); - $array['following'] = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); + public function get_inbox() { + return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) ); + } - return $array; + /** + * Returns the Outbox-API-Endpoint. + * + * @return string The Outbox-Endpoint. + */ + public function get_outbox() { + return get_rest_url_by_path( sprintf( 'users/%d/outbox', $this->get__id() ) ); + } + + /** + * Returns the Followers-API-Endpoint. + * + * @return string The Followers-Endpoint. + */ + public function get_followers() { + return get_rest_url_by_path( sprintf( 'users/%d/followers', $this->get__id() ) ); + } + + /** + * Returns the Following-API-Endpoint. + * + * @return string The Following-Endpoint. + */ + public function get_following() { + return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) ); } /** * Extend the User-Output with Attachments. * - * @param array $array The User-Output. - * @param numeric $user_id The User-ID. - * * @return array The extended User-Output. */ - public function add_attachments( $array, $user_id ) { - $array['attachment'] = array(); + public function get_attachment() { + $array = array(); - $array['attachment']['blog_url'] = array( + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Blog', 'activitypub' ), 'value' => \html_entity_decode( @@ -221,22 +214,22 @@ class User extends Actor { ), ); - $array['attachment']['profile_url'] = array( + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Profile', 'activitypub' ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_author_posts_url( $user_id ), \PHP_URL_HOST ) . '', + '' . \wp_parse_url( \get_author_posts_url( $this->get__id() ), \PHP_URL_HOST ) . '', \ENT_QUOTES, 'UTF-8' ), ); - if ( \get_the_author_meta( 'user_url', $user_id ) ) { - $array['attachment']['user_url'] = array( + if ( \get_the_author_meta( 'user_url', $this->get__id() ) ) { + $array[] = array( 'type' => 'PropertyValue', 'name' => \__( 'Website', 'activitypub' ), 'value' => \html_entity_decode( - '' . \wp_parse_url( \get_the_author_meta( 'user_url', $user_id ), \PHP_URL_HOST ) . '', + '' . \wp_parse_url( \get_the_author_meta( 'user_url', $this->get__id() ), \PHP_URL_HOST ) . '', \ENT_QUOTES, 'UTF-8' ), From 75a77b3f5c5e0cdecddcc2e44bfeb5443b0e946b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 18:02:14 +0200 Subject: [PATCH 290/427] finalize account handling still missing: publishing --- includes/activity/class-base-object.php | 1 - includes/class-activitypub.php | 8 ++++---- includes/class-signature.php | 2 +- includes/functions.php | 2 +- includes/model/class-application-user.php | 4 ++-- includes/model/class-blog-user.php | 8 ++++++-- includes/model/class-user.php | 19 ++++++++++++++++++- includes/rest/class-users.php | 4 ++++ includes/rest/class-webfinger.php | 1 - ...-class-activitypub-activity-dispatcher.php | 1 + ...typub-rest-post-signature-verification.php | 4 ++-- 11 files changed, 39 insertions(+), 15 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 0ad4457..c8520ba 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -472,7 +472,6 @@ class Base_Object { public function get( $key ) { if ( ! $this->has( $key ) ) { return new WP_Error( 'invalid_key', 'Invalid key' ); - } return $this->$key; diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 8e72caa..cd84dc1 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -71,16 +71,16 @@ class Activitypub { * @return string The new path to the JSON template. */ public static function render_json_template( $template ) { + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return $template; + } + if ( ! is_activitypub_request() ) { return $template; } $json_template = false; - if ( ! \is_author() && ! \is_singular() && ! \is_home() ) { - return $template; - } - // check if user can publish posts if ( \is_author() && ! User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ) ) { return $template; diff --git a/includes/class-signature.php b/includes/class-signature.php index c1ccbdf..b9666cc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -107,7 +107,7 @@ class Signature { */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { $user = User_Factory::get_by_id( $user_id ); - $key = $user->get_private_key(); + $key = $user->get__private_key(); $url_parts = \wp_parse_url( $url ); diff --git a/includes/functions.php b/includes/functions.php index a66d44e..88b0e2b 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -237,7 +237,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( 'REST_REQUEST' ) && ! REST_REQUEST ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) && ! \REST_REQUEST ) { return false; } diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php index 9c53f9d..c4ffaca 100644 --- a/includes/model/class-application-user.php +++ b/includes/model/class-application-user.php @@ -39,7 +39,7 @@ class Application_User extends Blog_User { return $this::get_name(); } - public function get_public_key() { + public function get__public_key() { $key = \get_option( 'activitypub_application_user_public_key' ); if ( $key ) { @@ -58,7 +58,7 @@ class Application_User extends Blog_User { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_option( 'activitypub_application_user_private_key' ); if ( $key ) { diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a4d3374..f3fd095 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -150,7 +150,7 @@ class Blog_User extends User { return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); } - public function get_public_key() { + public function get__public_key() { $key = \get_option( 'activitypub_blog_user_public_key' ); if ( $key ) { @@ -169,7 +169,7 @@ class Blog_User extends User { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_option( 'activitypub_blog_user_private_key' ); if ( $key ) { @@ -193,4 +193,8 @@ class Blog_User extends User { public function get_attachment() { return array(); } + + public function get_canonical_url() { + return \home_url(); + } } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 2ee2258..10d10d6 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -123,6 +123,19 @@ class User extends Actor { } public function get_public_key() { + return array( + 'id' => $this->get_id() . '#main-key', + 'owner' => $this->get_id(), + 'publicKeyPem' => $this->get__public_key(), + ); + } + + /** + * @param int $this->get__id() + * + * @return mixed + */ + public function get__public_key() { $key = \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); if ( $key ) { @@ -139,7 +152,7 @@ class User extends Actor { * * @return mixed */ - public function get_private_key() { + public function get__private_key() { $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); if ( $key ) { @@ -242,4 +255,8 @@ class User extends Actor { public function get_resource() { return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); } + + public function get_canonical_url() { + return $this->get_url(); + } } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 66637b7..1dd08f8 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -67,6 +67,10 @@ class Users { */ \do_action( 'activitypub_outbox_pre' ); + $user->set_context( + \Activitypub\Model\Activity::CONTEXT + ); + $json = $user->to_array(); $response = new WP_REST_Response( $json, 200 ); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index ec76686..f124889 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -55,7 +55,6 @@ class Webfinger { $aliases = array( $user->get_url(), - $user->get_canonical_url(), $user->get_at_url(), ); diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 70ed304..61f8c2b 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -49,6 +49,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_mentions() { $post = \wp_insert_post( array( diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index 2b7fc29..3ee895e 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -44,7 +44,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $user = Activitypub\User_Factory::get_by_id( 1 ); - $public_key = $user->get_public_key(); + $public_key = $user->get__public_key(); // signature_verification $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, 'rsa-sha256' ) > 0; @@ -56,7 +56,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { $user = Activitypub\User_Factory::get_by_id( 1 ); - $public_key = $user->get_public_key(); + $public_key = $user->get__public_key(); // return ActivityPub Profile with signature return array( 'id' => $actor, From 1fe8c26b1d43bd8908507aa4c48be486eced3c2e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 19:38:19 +0200 Subject: [PATCH 291/427] ignore prefixed attributes --- includes/activity/class-base-object.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index c8520ba..ee98411 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -462,6 +462,24 @@ class Base_Object { } } + /** + * Magic function, to transform the object to string. + * + * @return string The object id. + */ + public function __toString() { + return $this->to_string(); + } + + /** + * Function to transform the object to string. + * + * @return string The object id. + */ + public function to_string() { + return $this->get_id(); + } + /** * Generic getter. * @@ -575,6 +593,11 @@ class Base_Object { $vars = get_object_vars( $this ); foreach ( $vars as $key => $value ) { + // ignotre all _prefixed keys. + if ( '_' === substr( $key, 0, 1 ) ) { + continue; + } + // if value is empty, try to get it from a getter. if ( ! isset( $value ) ) { $value = call_user_func( array( $this, 'get_' . $key ) ); From 68e9bfdc7996921c3e3ed00154e385cb07079318 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 28 Jun 2023 19:38:50 +0200 Subject: [PATCH 292/427] this is now part of the Base_Object --- includes/model/class-follower.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index e95aff8..297bb36 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -54,15 +54,6 @@ class Follower extends Actor { */ protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - /** - * Magic function to return the Actor-URL when the Object is used as a string - * - * @return string - */ - public function __toString() { - return $this->get_url(); - } - /** * Set new Error * From 1543c49c19eb05d52889cf8382cf2af2471e5649 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 14:54:45 +0200 Subject: [PATCH 293/427] some doc changes --- includes/activity/class-base-object.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index ee98411..4343782 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -11,15 +11,16 @@ use WP_Error; use function Activitypub\camel_to_snake_case; use function Activitypub\snake_to_camel_case; + /** - * ObjectType is an implementation of one of the + * Base_Object is an implementation of one of the * Activity Streams Core Types. * * The Object is the primary base type for the Activity Streams * vocabulary. * * Note: Object is a reserved keyword in PHP. It has been suffixed with - * 'Type' for this reason. + * 'Base_' for this reason. * * @see https://www.w3.org/TR/activitystreams-core/#object */ From 3e969c859ac9a815b97638ea620091cd3fa12b60 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 18:44:25 +0200 Subject: [PATCH 294/427] send blog-wide activities if enabled --- includes/class-scheduler.php | 49 ++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 293b610..0349e44 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -84,20 +84,43 @@ class Scheduler { return; } - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) - ); + // send User activities + if ( ! is_user_disabled( $activitypub_post->get_user_id() ) ) { + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $activity_type ) - ), - array( $activitypub_post ) - ); + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); + } + + // send Blog-User activities + if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + $activitypub_post->set_post_author( User_Factory::BLOG_USER_ID ); + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $activity_type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $activity_type ) + ), + array( $activitypub_post ) + ); + } } /** From ced8cd0e29c7e730777afd67c94e46ef07dfded0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 29 Jun 2023 19:10:49 +0200 Subject: [PATCH 295/427] send activities for blog-wide user --- includes/class-scheduler.php | 26 +++++++++++++------------- includes/model/class-post.php | 10 +++++++++- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 0349e44..94a74a8 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -68,35 +68,35 @@ class Scheduler { return; } - $activitypub_post = new Post( $post ); - - $activity_type = false; + $type = false; if ( 'publish' === $new_status && 'publish' !== $old_status ) { - $activity_type = 'Create'; + $type = 'Create'; } elseif ( 'publish' === $new_status ) { - $activity_type = 'Update'; + $type = 'Update'; } elseif ( 'trash' === $new_status ) { - $activity_type = 'Delete'; + $type = 'Delete'; } - if ( ! $activity_type ) { + if ( ! $type ) { return; } // send User activities - if ( ! is_user_disabled( $activitypub_post->get_user_id() ) ) { + if ( ! is_user_disabled( $post->post_author ) ) { + $activitypub_post = new Post( $post ); + \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) + array( $activitypub_post, $type ) ); \wp_schedule_single_event( \time(), sprintf( 'activitypub_send_%s_activity', - \strtolower( $activity_type ) + \strtolower( $type ) ), array( $activitypub_post ) ); @@ -104,19 +104,19 @@ class Scheduler { // send Blog-User activities if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { - $activitypub_post->set_post_author( User_Factory::BLOG_USER_ID ); + $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $activity_type ) + array( $activitypub_post, $type ) ); \wp_schedule_single_event( \time(), sprintf( 'activitypub_send_%s_activity', - \strtolower( $activity_type ) + \strtolower( $type ) ), array( $activitypub_post ) ); diff --git a/includes/model/class-post.php b/includes/model/class-post.php index 5004162..03199ff 100644 --- a/includes/model/class-post.php +++ b/includes/model/class-post.php @@ -147,9 +147,17 @@ class Post { * Constructor * * @param WP_Post $post + * @param int $post_author */ - public function __construct( $post ) { + public function __construct( $post, $post_author = null ) { $this->post = \get_post( $post ); + + if ( $post_author ) { + $this->post_author = $post_author; + } else { + $this->post_author = $this->post->post_author; + } + $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) ); $this->add_to( get_rest_url_by_path( $path ) ); } From f207089269a5070be395785996269a9371cbbb7c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 30 Jun 2023 16:08:28 +0200 Subject: [PATCH 296/427] revert scheduler/dispatcher changes --- includes/class-scheduler.php | 51 ++++++++++-------------------------- 1 file changed, 14 insertions(+), 37 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 94a74a8..ddb68e1 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -82,45 +82,22 @@ class Scheduler { return; } - // send User activities - if ( ! is_user_disabled( $post->post_author ) ) { - $activitypub_post = new Post( $post ); + $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $type ) - ); + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $activitypub_post, $type ) + ); - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $activitypub_post ) - ); - } - - // send Blog-User activities - if ( ! is_user_disabled( User_Factory::BLOG_USER_ID ) ) { - $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); - - \wp_schedule_single_event( - \time(), - 'activitypub_send_activity', - array( $activitypub_post, $type ) - ); - - \wp_schedule_single_event( - \time(), - sprintf( - 'activitypub_send_%s_activity', - \strtolower( $type ) - ), - array( $activitypub_post ) - ); - } + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $type ) + ), + array( $activitypub_post ) + ); } /** From dd67f76db199fcdc069dbbfeac574c5de56bae03 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 30 Jun 2023 16:12:04 +0200 Subject: [PATCH 297/427] fix class names --- includes/class-scheduler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ddb68e1..db2d28c 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; +use Activitypub\User_Factory; use Activitypub\Collection\Followers; /** @@ -82,7 +83,7 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); + $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); \wp_schedule_single_event( \time(), From 359eabf67149448eee8f2086e9687078142b5537 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 11:20:44 +0200 Subject: [PATCH 298/427] use collection instead of factory --- includes/activity/class-activity.php | 165 ++++++++++++++++++ includes/activity/class-actor.php | 8 + includes/class-activity-dispatcher.php | 2 +- includes/class-activitypub.php | 2 +- includes/class-http.php | 4 +- includes/class-scheduler.php | 3 +- includes/class-signature.php | 6 +- includes/class-webfinger.php | 2 +- .../class-users.php} | 4 +- includes/functions.php | 4 +- includes/model/class-application-user.php | 4 +- includes/model/class-blog-user.php | 4 +- includes/model/class-post.php | 4 +- includes/model/class-user.php | 4 +- includes/peer/class-users.php | 67 ------- includes/rest/class-followers.php | 4 +- includes/rest/class-following.php | 4 +- includes/rest/class-inbox.php | 6 +- includes/rest/class-outbox.php | 4 +- includes/rest/class-users.php | 7 +- includes/rest/class-webfinger.php | 4 +- includes/table/class-followers.php | 4 +- includes/transformer/class-wp-user.php | 76 ++++++++ templates/admin-header.php | 2 +- templates/author-json.php | 2 +- templates/settings.php | 2 +- templates/welcome.php | 4 +- ...typub-rest-post-signature-verification.php | 4 +- 28 files changed, 295 insertions(+), 111 deletions(-) create mode 100644 includes/activity/class-activity.php rename includes/{class-user-factory.php => collection/class-users.php} (98%) delete mode 100644 includes/peer/class-users.php create mode 100644 includes/transformer/class-wp-user.php diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php new file mode 100644 index 0000000..592bbc1 --- /dev/null +++ b/includes/activity/class-activity.php @@ -0,0 +1,165 @@ + 'as:manuallyApprovesFollowers', + 'PropertyValue' => 'schema:PropertyValue', + 'schema' => 'http://schema.org#', + 'pt' => 'https://joinpeertube.org/ns#', + 'toot' => 'http://joinmastodon.org/ns#', + 'value' => 'schema:value', + 'Hashtag' => 'as:Hashtag', + 'featured' => array( + '@id' => 'toot:featured', + '@type' => '@id', + ), + 'featuredTags' => array( + '@id' => 'toot:featuredTags', + '@type' => '@id', + ), + ), + ); + + /** + * The object's unique global identifier + * + * @see https://www.w3.org/TR/activitypub/#obj-id + * + * @var string + */ + protected $id; + + /** + * @var string + */ + protected $type = 'Activity'; + + /** + * The context within which the object exists or an activity was + * performed. + * The notion of "context" used is intentionally vague. + * The intended function is to serve as a means of grouping objects + * and activities that share a common originating context or + * purpose. An example could be all activities relating to a common + * project or event. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $context = self::CONTEXT; + + /** + * Describes the direct object of the activity. + * For instance, in the activity "John added a movie to his + * wishlist", the object of the activity is the movie added. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $object; + + /** + * Describes one or more entities that either performed or are + * expected to perform the activity. + * Any single activity can have multiple actors. + * The actor MAY be specified using an indirect Link. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor + * + * @var string + * | \ActivityPhp\Type\Extended\AbstractActor + * | array + * | array + * | Link + */ + protected $actor; + + /** + * The indirect object, or target, of the activity. + * The precise meaning of the target is largely dependent on the + * type of action being described but will often be the object of + * the English preposition "to". + * For instance, in the activity "John added a movie to his + * wishlist", the target of the activity is John's wishlist. + * An activity can have more than one target. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target + * + * @var string + * | ObjectType + * | array + * | Link + * | array + */ + protected $target; + + /** + * Describes the result of the activity. + * For instance, if a particular action results in the creation of + * a new resource, the result property can be used to describe + * that new resource. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $result; + + /** + * An indirect object of the activity from which the + * activity is directed. + * The precise meaning of the origin is the object of the English + * preposition "from". + * For instance, in the activity "John moved an item to List B + * from List A", the origin of the activity is "List A". + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $origin; + + /** + * One or more objects used (or to be used) in the completion of an + * Activity. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument + * + * @var string + * | ObjectType + * | Link + * | null + */ + protected $instrument; +} diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 202783f..fabd653 100644 --- a/includes/activity/class-actor.php +++ b/includes/activity/class-actor.php @@ -7,6 +7,14 @@ namespace Activitypub\Activity; +/** + * \Activitypub\Activity\Actor is an implementation of + * one an Activity Streams Actor. + * + * Represents an individual actor. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + */ class Actor extends Base_Object { /** * @var string diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 00ffd48..c7862c0 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -3,7 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; use Activitypub\Model\Activity; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use function Activitypub\safe_remote_post; diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index cd84dc1..3b4d68a 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -82,7 +82,7 @@ class Activitypub { $json_template = false; // check if user can publish posts - if ( \is_author() && ! User_Factory::get_by_id( \get_the_author_meta( 'ID' ) ) ) { + if ( \is_author() && ! Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) { return $template; } diff --git a/includes/class-http.php b/includes/class-http.php index 240e5ea..dd19f9b 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -2,7 +2,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub HTTP Class @@ -63,7 +63,7 @@ class Http { */ public static function get( $url ) { $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( User_Factory::APPLICATION_USER_ID, 'get', $url, $date ); + $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index ddb68e1..ed0dbda 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -3,6 +3,7 @@ namespace Activitypub; use Activitypub\Model\Post; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers; /** @@ -82,7 +83,7 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, Users::BLOG_USER_ID ); + $activitypub_post = new Post( $post ); \wp_schedule_single_event( \time(), diff --git a/includes/class-signature.php b/includes/class-signature.php index b9666cc..b7c98fc 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -5,7 +5,7 @@ use WP_Error; use DateTime; use DateTimeZone; use Activitypub\Model\User; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub Signature Class @@ -106,7 +106,7 @@ class Signature { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); $key = $user->get__private_key(); $url_parts = \wp_parse_url( $url ); @@ -136,7 +136,7 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); $key_id = $user->get_url() . '#main-key'; if ( ! empty( $digest ) ) { diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 958f544..4a3a1a6 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -24,7 +24,7 @@ class Webfinger { return \get_webfinger_resource( $user_id, false ); } - $user = User_Factory::get_by_id( $user_id ); + $user = Users::get_by_id( $user_id ); if ( ! $user ) { return ''; } diff --git a/includes/class-user-factory.php b/includes/collection/class-users.php similarity index 98% rename from includes/class-user-factory.php rename to includes/collection/class-users.php index 8cc9f26..62b4264 100644 --- a/includes/class-user-factory.php +++ b/includes/collection/class-users.php @@ -1,5 +1,5 @@ get_user_id() ); + $user = Users::get_by_id( $this->get_user_id() ); return $user->get_url(); } diff --git a/includes/model/class-user.php b/includes/model/class-user.php index 10d10d6..c66e3d8 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -5,7 +5,7 @@ use WP_Query; use WP_Error; use Activitypub\Signature; use Activitypub\Model\User; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Activity\Actor; use function Activitypub\is_user_disabled; @@ -13,7 +13,7 @@ use function Activitypub\get_rest_url_by_path; class User extends Actor { /** - * The User-ID + * The local User-ID (WP_User). * * @var int */ diff --git a/includes/peer/class-users.php b/includes/peer/class-users.php deleted file mode 100644 index fd9c7b8..0000000 --- a/includes/peer/class-users.php +++ /dev/null @@ -1,67 +0,0 @@ -wp_rewrite_rules(); - - // not using rewrite rules, and 'author=N' method failed, so we're out of options - if ( empty( $rewrite ) ) { - return 0; - } - - // generate rewrite rule for the author url - $author_rewrite = $wp_rewrite->get_author_permastruct(); - $author_regexp = \str_replace( '%author%', '', $author_rewrite ); - - // match the rewrite rule with the passed url - if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) { - $user = \get_user_by( 'slug', $match[2] ); - if ( $user ) { - return $user->ID; - } - } - - return 0; - } -} diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 477393e..c359391 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,7 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -52,7 +52,7 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 6f13482..10f4bed 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,7 +1,7 @@ get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b8d75c9..eee740c 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Model\Activity; use function Activitypub\get_context; @@ -74,7 +74,7 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -126,7 +126,7 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 1dc05c7..3df982f 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,7 +5,7 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Model\Post; use Activitypub\Model\Activity; @@ -53,7 +53,7 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = Users::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 1dd08f8..dac7792 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,7 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\User_Factory; +use \Activitypub\Model\Activity; +use Activitypub\Collection\User_Collection; use function Activitypub\is_activitypub_request; @@ -50,7 +51,7 @@ class Users { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = User_Factory::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -68,7 +69,7 @@ class Users { \do_action( 'activitypub_outbox_pre' ); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + Activity::CONTEXT ); $json = $user->to_array(); diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index f124889..1dc79c9 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,7 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; /** * ActivityPub WebFinger REST-Class @@ -47,7 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = User_Factory::get_by_resource( $resource ); + $user = Users::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index f16a479..93d9456 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -2,7 +2,7 @@ namespace Activitypub\Table; use WP_List_Table; -use Activitypub\User_Factory; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers as FollowerCollection; if ( ! \class_exists( '\WP_List_Table' ) ) { @@ -14,7 +14,7 @@ class Followers extends WP_List_Table { public function __construct() { if ( get_current_screen()->id === 'settings_page_activitypub' ) { - $this->user_id = User_Factory::BLOG_USER_ID; + $this->user_id = Users::BLOG_USER_ID; } else { $this->user_id = \get_current_user_id(); } diff --git a/includes/transformer/class-wp-user.php b/includes/transformer/class-wp-user.php new file mode 100644 index 0000000..083b119 --- /dev/null +++ b/includes/transformer/class-wp-user.php @@ -0,0 +1,76 @@ +wp_user = $wp_user; + } + + public function to_user() { + $wp_user = $this->wp_user; + if ( + is_user_disabled( $user->ID ) || + ! get_user_by( 'id', $user->ID ) + ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $user = new User(); + + $user->setwp_user->ID( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); + $user->set_url( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); + $user->set_summary( $this->get_summary() ); + $user->set_name( \esc_attr( $wp_user->display_name ) ); + $user->set_preferred_username( \esc_attr( $wp_user->login ) ); + + $user->set_icon( $this->get_icon() ); + $user->set_image( $this->get_image() ); + + return $user; + } + + public function get_summary() { + $description = get_user_meta( $this->wp_user->ID, 'activitypub_user_description', true ); + if ( empty( $description ) ) { + $description = $this->wp_user->description; + } + return \wpautop( \wp_kses( $description, 'default' ) ); + } + + public function get_icon() { + $icon = \esc_url( + \get_avatar_url( + $this->wp_user->ID, + array( 'size' => 120 ) + ) + ); + + return array( + 'type' => 'Image', + 'url' => $icon, + ); + } + + public function get_image() { + if ( \has_header_image() ) { + $image = \esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => $image, + ); + } + + return null; + } +} diff --git a/templates/admin-header.php b/templates/admin-header.php index f2e7ed5..3b40468 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -12,7 +12,7 @@ - + diff --git a/templates/author-json.php b/templates/author-json.php index 3defd98..6c5f8ad 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,5 +1,5 @@ set_context( \Activitypub\Model\Activity::CONTEXT diff --git a/templates/settings.php b/templates/settings.php index 162ea02..b587b9e 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,7 +31,7 @@ - +

    diff --git a/templates/welcome.php b/templates/welcome.php index 0bce61c..02e8be3 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -15,7 +15,7 @@

    - +

    @@ -44,7 +44,7 @@

    ID ); + $user = \Activitypub\Collection\Users::get_by_id( wp_get_current_user()->ID ); echo wp_kses( \sprintf( // translators: diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index 3ee895e..f39927d 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -42,7 +42,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { $signed_headers = $signature_block['headers']; $signed_data = Activitypub\Signature::get_signed_data( $signed_headers, $signature_block, $headers ); - $user = Activitypub\User_Factory::get_by_id( 1 ); + $user = Activitypub\Collection\Users::get_by_id( 1 ); $public_key = $user->get__public_key(); @@ -55,7 +55,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { add_filter( 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { - $user = Activitypub\User_Factory::get_by_id( 1 ); + $user = Activitypub\Collection\Users::get_by_id( 1 ); $public_key = $user->get__public_key(); // return ActivityPub Profile with signature return array( From 1685ec7cc8725bf67d246f82a5a12b8c9ef333d4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 11:56:25 +0200 Subject: [PATCH 299/427] allow sending blog-wide activities --- includes/class-activity-dispatcher.php | 95 +++++++++++-------- includes/class-scheduler.php | 6 +- ...-class-activitypub-activity-dispatcher.php | 10 +- 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 00ffd48..618d668 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -1,11 +1,14 @@ from_post( $activitypub_post ); + if ( is_user_disabled( $wp_post->post_author ) ) { + return; + } - $user_id = $activitypub_post->get_user_id(); + $post = new Post( $wp_post ); + + $activitypub_activity = new Activity( $activity_type ); + $activitypub_activity->from_post( $post ); + + $user_id = $wp_post->post_author; + $follower_inboxes = Followers::get_inboxes( $user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); + + foreach ( $inboxes as $inbox ) { + $activity = $activitypub_activity->to_json(); + + safe_remote_post( $inbox, $activity, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $activity_type The Activity-Type. + * + * @return void + */ + public static function send_blog_activity( WP_Post $wp_post, $activity_type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + if ( is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + return; + } + + $post = new Post( $wp_post, User_Factory::BLOG_USER_ID ); + + $activitypub_activity = new Activity( $activity_type ); + $activitypub_activity->from_post( $post ); + + $user_id = User_Factory::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index db2d28c..fa8ccc2 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -83,12 +83,10 @@ class Scheduler { return; } - $activitypub_post = new Post( $post, User_Factory::BLOG_USER_ID ); - \wp_schedule_single_event( \time(), 'activitypub_send_activity', - array( $activitypub_post, $type ) + array( $post, $type ) ); \wp_schedule_single_event( @@ -97,7 +95,7 @@ class Scheduler { 'activitypub_send_%s_activity', \strtolower( $type ) ), - array( $activitypub_post ) + array( $post ) ); } diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 61f8c2b..cb76c17 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,10 +34,9 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); + $post = get_post( $post ); - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( $post, 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -77,10 +76,9 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); + $post = get_post( $post ); - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( $post, 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); From 493b8ffad5925539511718c616e7ab0a1ddcbdba Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 17:59:42 +0200 Subject: [PATCH 300/427] use transformer instead of post-model --- includes/activity/class-activity.php | 38 ++ includes/activity/class-base-object.php | 15 + includes/class-activity-dispatcher.php | 96 ++-- includes/class-activitypub.php | 1 + includes/class-admin.php | 2 - includes/class-scheduler.php | 8 +- includes/collection/class-followers.php | 2 +- includes/model/class-activity.php | 244 ---------- includes/model/class-user.php | 1 - includes/rest/class-inbox.php | 2 +- includes/rest/class-outbox.php | 20 +- includes/rest/class-users.php | 4 +- .../{model => transformer}/class-post.php | 420 ++++++------------ includes/transformer/class-wp-user.php | 76 ---- templates/author-json.php | 2 +- templates/blog-json.php | 2 +- templates/post-json.php | 4 +- ...-class-activitypub-activity-dispatcher.php | 10 +- tests/test-class-activitypub-activity.php | 7 +- tests/test-class-activitypub-post.php | 4 +- ...typub-rest-post-signature-verification.php | 14 +- 21 files changed, 282 insertions(+), 690 deletions(-) delete mode 100644 includes/model/class-activity.php rename includes/{model => transformer}/class-post.php (55%) delete mode 100644 includes/transformer/class-wp-user.php diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index 592bbc1..a1bd3c5 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -7,6 +7,8 @@ namespace Activitypub\Activity; +use Activitypub\Activity\Base_Object; + /** * \Activitypub\Activity\Activity implements the common * attributes of an Activity. @@ -162,4 +164,40 @@ class Activity extends Base_Object { * | null */ protected $instrument; + + /** + * Set the object and copy Object properties to the Activity. + * + * Any to, bto, cc, bcc, and audience properties specified on the object + * MUST be copied over to the new Create activity by the server. + * + * @see https://www.w3.org/TR/activitypub/#object-without-create + * + * @param \Activitypub\Activity\Base_Object $object + * + * @return void + */ + public function set_object( Base_Object $object ) { + parent::set_object( $object ); + + foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { + $this->set( $i, $object->get( $i ) ); + } + + if ( $object->get_published() && ! $this->get_published() ) { + $this->set( 'published', $object->get_published() ); + } + + if ( $object->get_updated() && ! $this->get_updated() ) { + $this->set( 'updated', $object->get_updated() ); + } + + if ( $object->attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->attributed_to() ); + } + + if ( $object->get_id() && ! $this->get_id() ) { + $this->set( 'id', $object->get_id() . '#activity' ); + } + } } diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 4343782..151ebf1 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -604,6 +604,10 @@ class Base_Object { $value = call_user_func( array( $this, 'get_' . $key ) ); } + if ( is_object( $value ) ) { + $value = $value->to_array(); + } + // if value is still empty, ignore it for the array and continue. if ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; @@ -625,4 +629,15 @@ class Base_Object { return $array; } + + /** + * Convert Object to JSON. + * + * @return string The JSON string. + */ + public function to_json() { + $array = $this->to_array(); + + return \wp_json_encode( $array ); + } } diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index c7862c0..2a5444a 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -1,10 +1,11 @@ from_post( $activitypub_post ); + $object = Post::transform( $post )->to_object(); - $user_id = $activitypub_post->get_user_id(); + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); + + $user_id = $post->post_author; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - foreach ( $inboxes as $inbox ) { - $activity = $activitypub_activity->to_json(); + $array = $activity->to_json(); - safe_remote_post( $inbox, $activity, $user_id ); + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $array, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_blog_activity( WP_Post $post, $type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + $user = Users::get_by_id( Users::BLOG_USER_ID ); + + $object = Post::transform( $post )->to_object(); + $object->set_attributed_to( $user->get_url() ); + + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); + + $follower_inboxes = Followers::get_inboxes( $user->get__id() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); + + $array = $activity->to_array(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $array, $user_id ); } } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 3b4d68a..507da58 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -2,6 +2,7 @@ namespace Activitypub; use Activitypub\Signature; +use Activitypub\Collection\Users; /** * ActivityPub Class diff --git a/includes/class-admin.php b/includes/class-admin.php index 1a94aa3..647805a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,8 +1,6 @@ 'as:manuallyApprovesFollowers', - 'PropertyValue' => 'schema:PropertyValue', - 'schema' => 'http://schema.org#', - 'pt' => 'https://joinpeertube.org/ns#', - 'toot' => 'http://joinmastodon.org/ns#', - 'value' => 'schema:value', - 'Hashtag' => 'as:Hashtag', - 'featured' => array( - '@id' => 'toot:featured', - '@type' => '@id', - ), - 'featuredTags' => array( - '@id' => 'toot:featuredTags', - '@type' => '@id', - ), - ), - ); - - /** - * The JSON-LD context. - * - * @var array - */ - private $context = self::CONTEXT; - - /** - * The published date. - * - * @var string - */ - private $published = ''; - - /** - * The Activity-ID. - * - * @var string - */ - private $id = ''; - - /** - * The Activity-Type. - * - * @var string - */ - private $type = 'Create'; - - /** - * The Activity-Actor. - * - * @var string - */ - private $actor = ''; - - /** - * The Audience. - * - * @var array - */ - private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - - /** - * The CC. - * - * @var array - */ - private $cc = array(); - - /** - * The Activity-Object. - * - * @var array - */ - private $object = null; - - /** - * The Class-Constructor. - * - * @param string $type The Activity-Type. - * @param boolean $context The JSON-LD context. - */ - public function __construct( $type = 'Create', $context = true ) { - if ( true !== $context ) { - $this->context = null; - } - - $this->type = \ucfirst( $type ); - $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \time() ); - } - - /** - * Magic Getter/Setter - * - * @param string $method The method name. - * @param string $params The method params. - * - * @return mixed The value. - */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - - if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { - if ( ! is_array( $this->$var ) ) { - $this->$var = $params[0]; - } - - if ( is_array( $params[0] ) ) { - $this->$var = array_merge( $this->$var, $params[0] ); - } else { - array_push( $this->$var, $params[0] ); - } - - $this->$var = array_unique( $this->$var ); - } - } - - /** - * Convert from a Post-Object. - * - * @param Post $post The Post-Object. - * - * @return void - */ - public function from_post( Post $post ) { - $this->object = $post->to_array(); - - if ( isset( $object['published'] ) ) { - $this->published = $object['published']; - } - - $path = sprintf( 'users/%d/followers', intval( $post->get_post_author() ) ); - $this->add_to( get_rest_url_by_path( $path ) ); - - if ( isset( $this->object['attributedTo'] ) ) { - $this->actor = $this->object['attributedTo']; - } - - foreach ( $post->get_tags() as $tag ) { - if ( 'Mention' === $tag['type'] ) { - $this->add_cc( $tag['href'] ); - } - } - - $type = \strtolower( $this->type ); - - if ( isset( $this->object['id'] ) ) { - $this->id = add_query_arg( 'activity', $type, $this->object['id'] ); - } - } - - public function from_comment( $object ) { - - } - - public function to_comment() { - - } - - public function from_remote_array( $array ) { - - } - - /** - * Convert to an Array. - * - * @return array The Array. - */ - public function to_array() { - $array = array_filter( \get_object_vars( $this ) ); - - if ( $this->context ) { - $array = array( '@context' => $this->context ) + $array; - } - - unset( $array['context'] ); - - return $array; - } - - /** - * Convert to JSON - * - * @return string The JSON. - */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } - - /** - * Convert to a Simple Array. - * - * @return string The array. - */ - public function to_simple_array() { - $activity = array( - '@context' => $this->context, - 'type' => $this->type, - 'actor' => $this->actor, - 'object' => $this->object, - 'to' => $this->to, - 'cc' => $this->cc, - ); - - if ( $this->id ) { - $activity['id'] = $this->id; - } - - return $activity; - } - - /** - * Convert to a Simple JSON. - * - * @return string The JSON. - */ - public function to_simple_json() { - return \wp_json_encode( $this->to_simple_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } -} diff --git a/includes/model/class-user.php b/includes/model/class-user.php index c66e3d8..d641497 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -4,7 +4,6 @@ namespace Activitypub\Model; use WP_Query; use WP_Error; use Activitypub\Signature; -use Activitypub\Model\User; use Activitypub\Collection\Users; use Activitypub\Activity\Actor; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index eee740c..4f0c3c3 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -5,7 +5,7 @@ use WP_Error; use WP_REST_Server; use WP_REST_Response; use Activitypub\Collection\Users; -use Activitypub\Model\Activity; +use Activitypub\Activity\Activity; use function Activitypub\get_context; use function Activitypub\url_to_authorid; diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 3df982f..91af46d 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; +use Activitypub\Activity\Activity; use Activitypub\Collection\Users; -use Activitypub\Model\Post; -use Activitypub\Model\Activity; +use Activitypub\Transformer\Post; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; @@ -73,9 +73,9 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - $json->actor = $user->get_id(); + //$json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user->get__id() ) ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore // phpcs:ignore @@ -97,18 +97,20 @@ class Outbox { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user->get__id(), + 'author' => $user_id, 'offset' => ( $page - 1 ) * 10, 'post_type' => $post_types, ) ); foreach ( $posts as $post ) { - $activitypub_post = new Post( $post ); - $activitypub_activity = new Activity( 'Create', false ); + $post = Post::transform( $post )->to_object(); + $activity = new Activity(); + $activity->set_type( 'Create' ); + $activity->set_context( null ); + $activity->set_object( $post ); - $activitypub_activity->from_post( $activitypub_post ); - $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore + $json->orderedItems[] = $activity->to_array(); // phpcs:ignore } } diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index dac7792..2017036 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,8 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use \Activitypub\Model\Activity; -use Activitypub\Collection\User_Collection; +use \Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\is_activitypub_request; diff --git a/includes/model/class-post.php b/includes/transformer/class-post.php similarity index 55% rename from includes/model/class-post.php rename to includes/transformer/class-post.php index 1aa862a..ca35949 100644 --- a/includes/model/class-post.php +++ b/includes/transformer/class-post.php @@ -1,84 +1,37 @@ array( 'href' => array(), 'title' => array(), @@ -126,185 +79,70 @@ class Post { ); /** - * List of audience + * Static function to Transform a WP_Post Object. * - * Also used for visibility + * This helps to chain the output of the Transformer. * - * @var array - */ - private $to = array( 'https://www.w3.org/ns/activitystreams#Public' ); - - /** - * List of audience - * - * Also used for visibility - * - * @var array - */ - private $cc = array(); - - /** - * Constructor - * - * @param WP_Post $post - * @param int $post_author - */ - public function __construct( $post, $post_author = null ) { - $this->post = \get_post( $post ); - - if ( $post_author ) { - $this->post_author = $post_author; - } else { - $this->post_author = $this->post->post_author; - } - - $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) ); - $this->add_to( get_rest_url_by_path( $path ) ); - } - - /** - * Magic function to implement getter and setter - * - * @param string $method - * @param string $params + * @param WP_Post $wp_post The WP_Post object * * @return void */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - if ( empty( $this->$var ) && ! empty( $this->post->$var ) ) { - return $this->post->$var; - } - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - $this->$var = $params[0]; - } - - if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { - if ( ! is_array( $this->$var ) ) { - $this->$var = $params[0]; - } - - if ( is_array( $params[0] ) ) { - $this->$var = array_merge( $this->$var, $params[0] ); - } else { - array_push( $this->$var, $params[0] ); - } - - $this->$var = array_unique( $this->$var ); - } + public static function transform( WP_Post $wp_post ) { + return new self( $wp_post ); } /** - * Returns the User ID. * - * @return int the User ID. + * + * @param WP_Post $wp_post */ - public function get_user_id() { - return apply_filters( 'activitypub_post_user_id', $this->get_post_author(), $this->post ); + public function __construct( WP_Post $wp_post ) { + $this->wp_post = $wp_post; } /** - * Converts this Object into an Array. + * Transforms the WP_Post object to an ActivityPub Object * - * @return array the array representation of a Post. + * @see \Activitypub\Activity\Base_Object + * + * @return \Activitypub\Activity\Base_Object The ActivityPub Object */ - public function to_array() { - $post = $this->post; + public function to_object() { + $wp_post = $this->wp_post; + $object = new Base_Object(); - $array = array( - 'id' => $this->get_id(), - 'url' => $this->get_url(), - 'type' => $this->get_object_type(), - 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), - 'attributedTo' => $this->get_actor(), - 'summary' => $this->get_summary(), - 'inReplyTo' => null, - 'content' => $this->get_content(), - 'contentMap' => array( + $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); + $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); + $object->set_type( $this->get_object_type() ); + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + $object->attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_content( $this->get_content() ); + $object->set_content_map( + array( \strstr( \get_locale(), '_', true ) => $this->get_content(), - ), - 'to' => $this->get_to(), - 'cc' => $this->get_cc(), - 'attachment' => $this->get_attachments(), - 'tag' => $this->get_tags(), + ) ); + $path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) ); - return \apply_filters( 'activitypub_post', $array, $this->post ); + $object->set_to( + array( + 'https://www.w3.org/ns/activitystreams#Public', + get_rest_url_by_path( $path ), + ) + ); + $object->set_cc( $this->get_cc() ); + $object->set_attachment( $this->get_attachments() ); + $object->set_tag( $this->get_tags() ); + + return $object; } /** - * Returns the Actor of this Object. + * Generates all Image Attachments for a Post. * - * @return string The URL of the Actor. + * @return array The Image Attachments. */ - public function get_actor() { - $user = Users::get_by_id( $this->get_user_id() ); - - return $user->get_url(); - } - - /** - * Converts this Object into a JSON String - * - * @return string - */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); - } - - /** - * Returns the URL of an Activity Object - * - * @return string - */ - public function get_url() { - if ( $this->url ) { - return $this->url; - } - - $post = $this->post; - - if ( 'trash' === get_post_status( $post ) ) { - $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); - } else { - $permalink = \get_permalink( $post ); - } - - $this->url = $permalink; - - return $permalink; - } - - /** - * Returns the ID of an Activity Object - * - * @return string - */ - public function get_id() { - if ( $this->id ) { - return $this->id; - } - - $this->id = $this->get_url(); - - return $this->id; - } - - /** - * Returns a list of Image Attachments - * - * @return array - */ - public function get_attachments() { - if ( $this->attachments ) { - return $this->attachments; - } - + protected function get_attachments() { $max_images = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) ); $images = array(); @@ -314,7 +152,7 @@ class Post { return $images; } - $id = $this->post->ID; + $id = $this->wp_post->ID; $image_ids = array(); @@ -393,7 +231,7 @@ class Post { * * @return array|false Array of image data, or boolean false if no image is available. */ - public function get_image( $id, $image_size = 'full' ) { + protected function get_image( $id, $image_size = 'full' ) { /** * Hook into the image retrieval process. Before image retrieval. * @@ -416,64 +254,22 @@ class Post { } /** - * Returns a list of Tags, used in the Post + * Returns the ActivityStreams 2.0 Object-Type for a Post based on the + * settings and the Post-Type. * - * @return array - */ - public function get_tags() { - if ( $this->tags ) { - return $this->tags; - } - - $tags = array(); - - $post_tags = \get_the_tags( $this->post->ID ); - if ( $post_tags ) { - foreach ( $post_tags as $post_tag ) { - $tag = array( - 'type' => 'Hashtag', - 'href' => \get_tag_link( $post_tag->term_id ), - 'name' => '#' . $post_tag->slug, - ); - $tags[] = $tag; - } - } - - $mentions = apply_filters( 'activitypub_extract_mentions', array(), $this->post->post_content, $this ); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => $url, - 'name' => $mention, - ); - $tags[] = $tag; - } - } - - $this->tags = $tags; - - return $tags; - } - - /** - * Returns the as2 object-type for a given post + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types * - * @return string the object-type + * @return string The Object-Type. */ - public function get_object_type() { - if ( $this->object_type ) { - return $this->object_type; - } - + protected function get_object_type() { if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) { return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); } - $post_type = \get_post_type( $this->post ); + $post_type = \get_post_type( $this->wp_post ); switch ( $post_type ) { case 'post': - $post_format = \get_post_format( $this->post ); + $post_format = \get_post_format( $this->wp_post ); switch ( $post_format ) { case 'aside': case 'status': @@ -519,25 +315,78 @@ class Post { break; } - $this->object_type = $object_type; - return $object_type; } + /** + * Returns a list of Mentions, used in the Post. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#Mention + * + * @return array The list of Mentions. + */ + protected function get_cc() { + $cc = array(); + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $cc[] = $url; + } + } + + return $cc; + } + + /** + * Returns a list of Tags, used in the Post. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tags() { + $tags = array(); + + $post_tags = \get_the_tags( $this->wp_post->ID ); + if ( $post_tags ) { + foreach ( $post_tags as $post_tag ) { + $tag = array( + 'type' => 'Hashtag', + 'href' => esc_url( \get_tag_link( $post_tag->term_id ) ), + 'name' => '#' . \esc_attr( $post_tag->slug ), + ); + $tags[] = $tag; + } + } + + $mentions = $this->get_mentions(); + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $tag = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + $tags[] = $tag; + } + } + + return $tags; + } + /** * Returns the content for the ActivityPub Item. * - * @return string the content + * The content will be generated based on the user settings. + * + * @return string The content. */ - public function get_content() { + protected function get_content() { global $post; - if ( $this->content ) { - return $this->content; - } - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->post; + $post = $this->wp_post; $content = $this->get_post_content_template(); // Fill in the shortcodes. @@ -553,17 +402,15 @@ class Post { $content = \apply_filters( 'activitypub_the_content', $content, $post ); $content = \html_entity_decode( $content, \ENT_QUOTES, 'UTF-8' ); - $this->content = $content; - return $content; } /** * Gets the template to use to generate the content of the activitypub item. * - * @return string the template + * @return string The Template. */ - public function get_post_content_template() { + protected function get_post_content_template() { if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) { return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]"; } @@ -578,4 +425,13 @@ class Post { return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); } + + /** + * Helper function to get the @-Mentions from the post content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post ); + } } diff --git a/includes/transformer/class-wp-user.php b/includes/transformer/class-wp-user.php deleted file mode 100644 index 083b119..0000000 --- a/includes/transformer/class-wp-user.php +++ /dev/null @@ -1,76 +0,0 @@ -wp_user = $wp_user; - } - - public function to_user() { - $wp_user = $this->wp_user; - if ( - is_user_disabled( $user->ID ) || - ! get_user_by( 'id', $user->ID ) - ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - $user = new User(); - - $user->setwp_user->ID( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); - $user->set_url( \esc_url( \get_author_posts_url( $wp_user->ID ) ) ); - $user->set_summary( $this->get_summary() ); - $user->set_name( \esc_attr( $wp_user->display_name ) ); - $user->set_preferred_username( \esc_attr( $wp_user->login ) ); - - $user->set_icon( $this->get_icon() ); - $user->set_image( $this->get_image() ); - - return $user; - } - - public function get_summary() { - $description = get_user_meta( $this->wp_user->ID, 'activitypub_user_description', true ); - if ( empty( $description ) ) { - $description = $this->wp_user->description; - } - return \wpautop( \wp_kses( $description, 'default' ) ); - } - - public function get_icon() { - $icon = \esc_url( - \get_avatar_url( - $this->wp_user->ID, - array( 'size' => 120 ) - ) - ); - - return array( - 'type' => 'Image', - 'url' => $icon, - ); - } - - public function get_image() { - if ( \has_header_image() ) { - $image = \esc_url( \get_header_image() ); - return array( - 'type' => 'Image', - 'url' => $image, - ); - } - - return null; - } -} diff --git a/templates/author-json.php b/templates/author-json.php index 6c5f8ad..70fb43b 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -2,7 +2,7 @@ $user = \Activitypub\Collection\Users::get_by_id( \get_the_author_meta( 'ID' ) ); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + \Activitypub\Activity\Activity::CONTEXT ); /* diff --git a/templates/blog-json.php b/templates/blog-json.php index 635e7d5..7ce6a27 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -2,7 +2,7 @@ $user = new \Activitypub\Model\Blog_User(); $user->set_context( - \Activitypub\Model\Activity::CONTEXT + \Activitypub\Activity\Activity::CONTEXT ); /* diff --git a/templates/post-json.php b/templates/post-json.php index 4c597d6..89467c4 100644 --- a/templates/post-json.php +++ b/templates/post-json.php @@ -2,8 +2,8 @@ // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $post = \get_post(); -$activitypub_post = new \Activitypub\Model\Post( $post ); -$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $activitypub_post->to_array() ); +$object = new \Activitypub\Transformer\Post( $post ); +$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() ); // filter output $json = \apply_filters( 'activitypub_json_post_array', $json ); diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 61f8c2b..f028798 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,10 +34,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); - - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -77,10 +74,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - \Activitypub\Activity_Dispatcher::send_create_activity( $activitypub_post ); - - $this->assertNotEmpty( $activitypub_post->get_content() ); + \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 8262f6c..b25545c 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -17,10 +17,11 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { 10 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); - $activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_activity = new \Activitypub\Activity\Activity(); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $this->assertContains( \Activitypub\get_rest_url_by_path( 'users/1/followers' ), $activitypub_activity->get_to() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() ); diff --git a/tests/test-class-activitypub-post.php b/tests/test-class-activitypub-post.php index 4f1c74e..e995afa 100644 --- a/tests/test-class-activitypub-post.php +++ b/tests/test-class-activitypub-post.php @@ -10,13 +10,13 @@ class Test_Activitypub_Post extends WP_UnitTestCase { $permalink = \get_permalink( $post ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); $this->assertEquals( $permalink, $activitypub_post->get_id() ); \wp_trash_post( $post ); - $activitypub_post = new \Activitypub\Model\Post( $post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); $this->assertEquals( $permalink, $activitypub_post->get_id() ); } diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php index f39927d..97f78f9 100644 --- a/tests/test-class-activitypub-rest-post-signature-verification.php +++ b/tests/test-class-activitypub-rest-post-signature-verification.php @@ -10,9 +10,10 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ) ); $remote_actor = \get_author_posts_url( 2 ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object(); + $activitypub_activity = new Activitypub\Activity\Activity( 'Create' ); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->add_cc( $remote_actor ); $activity = $activitypub_activity->to_json(); @@ -81,9 +82,10 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { ); $remote_actor = \get_author_posts_url( 2 ); $remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' ); - $activitypub_post = new \Activitypub\Model\Post( $post ); - $activitypub_activity = new Activitypub\Model\Activity( 'Create' ); - $activitypub_activity->from_post( $activitypub_post ); + $activitypub_post = \Activitypub\Transformer\Post::transform( \get_post( $post ) )->to_object(); + $activitypub_activity = new Activitypub\Activity\Activity(); + $activitypub_activity->set_type( 'Create' ); + $activitypub_activity->set_object( $activitypub_post ); $activitypub_activity->add_cc( $remote_actor_inbox ); $activity = $activitypub_activity->to_json(); From 7f3059427db0f48c6f49b278aec6333acd73d1ac Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 18:18:03 +0200 Subject: [PATCH 301/427] fix tests --- includes/class-activity-dispatcher.php | 37 +++++++++++++++----------- includes/functions.php | 14 ++++++---- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 692c487..e3ce52d 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -30,11 +30,11 @@ class Activity_Dispatcher { * Send Activities to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. + * @param string $type The Activity-Type. * * @return void */ - public static function send_user_activity( WP_Post $wp_post, $activity_type ) { + public static function send_user_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -42,22 +42,23 @@ class Activity_Dispatcher { return; } - $post = new Post( $wp_post ); + $object = Post::transform( $wp_post )->to_object(); - $activitypub_activity = new Activity( $activity_type ); - $activitypub_activity->from_post( $post ); + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); $user_id = $wp_post->post_author; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - $array = $activity->to_json(); + $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $array, $user_id ); + safe_remote_post( $inbox, $json, $user_id ); } } @@ -65,11 +66,11 @@ class Activity_Dispatcher { * Send Activities to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. + * @param string $type The Activity-Type. * * @return void */ - public static function send_blog_activity( WP_Post $wp_post, $activity_type ) { + public static function send_blog_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -77,22 +78,26 @@ class Activity_Dispatcher { return; } - $post = new Post( $wp_post, User_Factory::BLOG_USER_ID ); + $user = User_Factory::get_user( User_Factory::BLOG_USER_ID ); - $activitypub_activity = new Activity( $activity_type ); - $activitypub_activity->from_post( $post ); + $object = Post::transform( $wp_post )->to_object(); + $object->set_attributed_to( $user->get_id() ); + + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); $user_id = User_Factory::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); $inboxes = array_unique( $inboxes ); - $array = $activity->to_json(); + $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $array, $user_id ); + safe_remote_post( $inbox, $json, $user_id ); } } } diff --git a/includes/functions.php b/includes/functions.php index 85cf94a..65279cb 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,23 +1,27 @@ Date: Mon, 3 Jul 2023 19:25:49 +0200 Subject: [PATCH 302/427] consistent use of namespaces --- includes/rest/class-outbox.php | 2 +- includes/transformer/class-post.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 91af46d..a8708e5 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Activity\Activity; use Activitypub\Collection\Users; use Activitypub\Transformer\Post; +use Activitypub\Activity\Activity; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index ca35949..88295c8 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -2,8 +2,8 @@ namespace Activitypub\Transformer; use WP_Post; -use \Activitypub\Activity\Base_Object; -use \Activitypub\Collection\Users; +use Activitypub\Collection\Users; +use Activitypub\Activity\Base_Object; use function Activitypub\get_rest_url_by_path; From 47957c2a6a31030072f68af84b0ce6fa397e9bc4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 19:52:54 +0200 Subject: [PATCH 303/427] fix code --- includes/class-scheduler.php | 2 +- includes/rest/class-outbox.php | 6 +++--- includes/rest/class-users.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 7603441..271c4b6 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -4,7 +4,7 @@ namespace Activitypub; use Activitypub\Collection\Users; use Activitypub\Collection\Followers; -use \Activitypub\Transformer\Post; +use Activitypub\Transformer\Post; /** * ActivityPub Scheduler Class diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index a8708e5..8485647 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,9 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; use Activitypub\Transformer\Post; use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\get_context; use function Activitypub\get_rest_url_by_path; @@ -53,7 +53,7 @@ class Outbox { */ public static function user_outbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; @@ -73,7 +73,7 @@ class Outbox { $json->{'@context'} = get_context(); $json->id = \home_url( \add_query_arg( null, null ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); - //$json->actor = $user->get_id(); + $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index 2017036..b4f85fe 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -4,7 +4,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use \Activitypub\Activity\Activity; +use Activitypub\Activity\Activity; use Activitypub\Collection\Users as User_Collection; use function Activitypub\is_activitypub_request; From be07574cfea7e6c320654074363d05f6a9401ae3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 19:56:06 +0200 Subject: [PATCH 304/427] fix code --- includes/rest/class-followers.php | 4 ++-- includes/rest/class-following.php | 4 ++-- includes/rest/class-inbox.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index c359391..38b4746 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,7 +5,7 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; +use Activitypub\Collection\Users as User_Collection; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\get_rest_url_by_path; @@ -52,7 +52,7 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 10f4bed..29e7e07 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -1,7 +1,7 @@ get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 4f0c3c3..7cdb0b7 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -4,8 +4,8 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Users; use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\get_context; use function Activitypub\url_to_authorid; @@ -74,7 +74,7 @@ class Inbox { */ public static function user_inbox_get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; From 52e644631a57d4e20ec2d754d01abef75224c13d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 20:00:47 +0200 Subject: [PATCH 305/427] add missing `attributed_to` --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 88295c8..f9f36f3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -115,7 +115,7 @@ class Post { $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); - $object->attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); $object->set_content( $this->get_content() ); $object->set_content_map( array( From 07b0ae6e2d766cb016ef3803316c27deb9697763 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 3 Jul 2023 20:02:00 +0200 Subject: [PATCH 306/427] fix namespaces --- includes/rest/class-webfinger.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 1dc79c9..5e74308 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -3,7 +3,7 @@ namespace Activitypub\Rest; use WP_Error; use WP_REST_Response; -use Activitypub\Collection\Users; +use Activitypub\Collection\Users as User_Collection; /** * ActivityPub WebFinger REST-Class @@ -47,7 +47,7 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = Users::get_by_resource( $resource ); + $user = User_Collection::get_by_resource( $resource ); if ( is_wp_error( $user ) ) { return $user; From e65b70763db0a3743e7cbda8cdce26629f592cf3 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 12:18:48 +0200 Subject: [PATCH 307/427] use URL as post-name --- includes/model/class-follower.php | 1 + 1 file changed, 1 insertion(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 297bb36..5dd7e2a 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -148,6 +148,7 @@ class Follower extends Actor { 'post_title' => $this->get_name(), 'post_author' => 0, 'post_type' => Followers::POST_TYPE, + 'post_name' => $this->get_id(), 'post_content' => $this->get_summary(), 'post_status' => 'publish', 'meta_input' => $this->get_post_meta_input(), From 7a360dbf6f5b11b23fef3902318092da21261648 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:31:06 +0200 Subject: [PATCH 308/427] fix object handling --- includes/activity/class-activity.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index a1bd3c5..37a469f 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -79,7 +79,7 @@ class Activity extends Base_Object { * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term * * @var string - * | ObjectType + * | Base_Objectr * | Link * | null */ @@ -173,12 +173,16 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitypub/#object-without-create * - * @param \Activitypub\Activity\Base_Object $object + * @param string|Base_Objectr|Link|null $object * * @return void */ - public function set_object( Base_Object $object ) { - parent::set_object( $object ); + public function set_object( $object ) { + $this->set( 'object', $object ); + + if ( ! is_object( $object ) ) { + return; + } foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { $this->set( $i, $object->get( $i ) ); From 1380025d4ad2ef9332e0c13bcb1a3683c971fa1a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:31:45 +0200 Subject: [PATCH 309/427] always use Followers::add_follower to not ran into inconsistencies --- includes/class-migration.php | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/includes/class-migration.php b/includes/class-migration.php index d3c0fcb..2e5f19c 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,7 +1,6 @@ set_error( $meta ); - } - - $follower->upsert(); - - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + Followers::add_follower( $user_id, $actor ); } } } From 52038c9f43f1ea3356b937869e492317888fb4db Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:32:26 +0200 Subject: [PATCH 310/427] fix image and username handling --- includes/model/class-blog-user.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index c3b6cb1..2549fc4 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -66,7 +66,7 @@ class Blog_User extends User { * @return string The User-Url. */ public function get_url() { - return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); } /** @@ -116,17 +116,29 @@ class Blog_User extends User { return $default; } - public function get_username() { + public function get_preferred_username() { return self::get_default_username(); } - public function get_avatar() { - return \esc_url( \get_site_icon_url( 120 ) ); + public function get_icon() { + $image = wp_get_attachment_image_src( get_theme_mod( 'custom_logo' ) ); + + if ( $image ) { + return array( + 'type' => 'Image', + 'url' => esc_url( $image[0] ), + ); + } + + return null; } public function get_header_image() { if ( \has_header_image() ) { - return esc_url( \get_header_image() ); + return array( + 'type' => 'Image', + 'url' => esc_url( \get_header_image() ), + ); } return null; From 862de71cd21d40a75c36db2d6c1ae5b9c253f2d1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:32:49 +0200 Subject: [PATCH 311/427] fix WebFinger for pseudo-users --- includes/rest/class-webfinger.php | 83 ++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 5e74308..846ac50 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -18,7 +18,8 @@ class Webfinger { */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); - \add_action( 'webfinger_user_data', array( self::class, 'add_webfinger_discovery' ), 10, 3 ); + \add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 10, 3 ); + \add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 99, 2 ); } /** @@ -47,35 +48,9 @@ class Webfinger { */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); - $user = User_Collection::get_by_resource( $resource ); + $response = self::get_profile( $resource ); - if ( is_wp_error( $user ) ) { - return $user; - } - - $aliases = array( - $user->get_url(), - $user->get_at_url(), - ); - - $json = array( - 'subject' => $resource, - 'aliases' => array_values( array_unique( $aliases ) ), - 'links' => array( - array( - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $user->get_url(), - ), - array( - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => $user->get_url(), - ), - ), - ); - - return new WP_REST_Response( $json, 200 ); + return new WP_REST_Response( $response, 200 ); } /** @@ -102,7 +77,7 @@ class Webfinger { * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user */ - public static function add_webfinger_discovery( $array, $resource, $user ) { + public static function add_user_discovery( $array, $resource, $user ) { $array['links'][] = array( 'rel' => 'self', 'type' => 'application/activity+json', @@ -111,4 +86,52 @@ class Webfinger { return $array; } + + /** + * Add WebFinger discovery links + * + * @param array $array the jrd array + * @param string $resource the WebFinger resource + * @param WP_User $user the WordPress user + */ + public static function add_pseudo_user_discovery( $array, $resource ) { + if ( ! $array ) { + $array = array(); + } + + $profile = self::get_profile( $resource ); + + return array_merge( $array, $profile ); + } + + public static function get_profile( $resource ) { + $user = User_Collection::get_by_resource( $resource ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + $aliases = array( + $user->get_url(), + ); + + $profile = array( + 'subject' => $resource, + 'aliases' => array_values( array_unique( $aliases ) ), + 'links' => array( + array( + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => $user->get_url(), + ), + array( + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $user->get_url(), + ), + ), + ); + + return $profile; + } } From eed43355b3b2ab82f32866cadb15c227ca4fac04 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:33:07 +0200 Subject: [PATCH 312/427] fix inbox --- includes/rest/class-inbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 7cdb0b7..b530a37 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -126,7 +126,7 @@ class Inbox { public static function user_inbox_post( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = Users::get_by_various( $user_id ); + $user = User_Collection::get_by_various( $user_id ); if ( is_wp_error( $user ) ) { return $user; From 1269cc6248b91911047c456bc133e5f6d27c8fb0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:33:16 +0200 Subject: [PATCH 313/427] better instancing --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index f9f36f3..b337f68 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -88,7 +88,7 @@ class Post { * @return void */ public static function transform( WP_Post $wp_post ) { - return new self( $wp_post ); + return new static( $wp_post ); } /** From 5c59834a0c622bcb841450afa425acf28b01ebcb Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 15:34:22 +0200 Subject: [PATCH 314/427] various fixes mainly regarding `send_follow_response` --- includes/collection/class-followers.php | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 5b67a7f..c63a28c 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -7,7 +7,9 @@ use WP_Query; use Activitypub\Http; use Activitypub\Webfinger; use Activitypub\Model\Follower; +use Activitypub\Collection\Users; use Activitypub\Activity\Activity; +use Activitypub\Activity\Base_Object; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -131,7 +133,7 @@ class Followers { '_actor', array( 'type' => 'string', - 'single' => false, + 'single' => true, 'sanitize_callback' => function( $value ) { return esc_sql( $value ); }, @@ -191,7 +193,7 @@ class Followers { public static function add_follower( $user_id, $actor ) { $meta = get_remote_metadata_by_actor( $actor ); - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + if ( is_tombstone( $meta ) ) { return $meta; } @@ -241,7 +243,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -280,17 +282,21 @@ class Followers { unset( $object['@context'] ); } + $user = Users::get_by_id( $user_id ); + // get inbox $inbox = $follower->get_inbox(); // send "Accept" activity - $activity = new Activity( 'Accept' ); - $activity->set_activity_object( $object ); - $activity->set_actor( \get_author_posts_url( $user_id ) ); + $activity = new Activity(); + $activity->set_type( 'Accept' ); + $activity->set_object( $object ); + $activity->set_actor( $user->get_id() ); $activity->set_to( $actor ); - $activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); + $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) ); + + $activity = $activity->to_json(); - $activity = $activity->to_simple_json(); $response = Http::post( $inbox, $activity, $user_id ); } From 19d60d8fec9ee0bbb92aaec010e9b21d4f573f42 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 16:16:31 +0200 Subject: [PATCH 315/427] fix sending activities --- includes/class-activity-dispatcher.php | 48 ++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index e3ce52d..5c91b04 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -74,20 +74,62 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); - if ( is_user_disabled( User_Factory::BLOG_USER_ID ) ) { + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } - $user = User_Factory::get_user( User_Factory::BLOG_USER_ID ); + $user = Users::get_by_id( Users::BLOG_USER_ID ); $object = Post::transform( $wp_post )->to_object(); $object->set_attributed_to( $user->get_id() ); $activity = new Activity(); $activity->set_type( $type ); + $activity->set_actor( $user->get_id() ); $activity->set_object( $object ); - $user_id = User_Factory::BLOG_USER_ID; + $user_id = Users::BLOG_USER_ID; + $follower_inboxes = Followers::get_inboxes( $user_id ); + $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); + + $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); + $inboxes = array_unique( $inboxes ); + + $json = $activity->to_json(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $user_id ); + } + } + + /** + * Send Activities to followers and mentioned users. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_blog_announce_activity( WP_Post $wp_post, $type ) { + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + return; + } + + $user = Users::get_by_id( Users::BLOG_USER_ID ); + + $object = Post::transform( $wp_post )->to_object(); + + $activity = new Activity(); + $activity->set_type( 'Announce' ); + $activity->set_actor( $user->get_id() ); + $activity->set_object( $object ); + + $activity->set_object( $object->get_id() ); + + $user_id = Users::BLOG_USER_ID; $follower_inboxes = Followers::get_inboxes( $user_id ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); From c1da689d6696c1865e3fdc542b79a2ce415312b4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 5 Jul 2023 18:13:46 +0200 Subject: [PATCH 316/427] fix `is_activitypub_request` function --- includes/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/functions.php b/includes/functions.php index 65279cb..c41304d 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -241,7 +241,7 @@ function is_activitypub_request() { * ActivityPub requests are currently only made for * author archives, singular posts, and the homepage. */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) && ! \REST_REQUEST ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) { return false; } From 96c1e921516feb0791222b48afa949a1992e60f2 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 6 Jul 2023 14:42:18 +0200 Subject: [PATCH 317/427] optimize and simplify followers --- includes/activity/class-base-object.php | 35 +++- includes/class-scheduler.php | 10 +- includes/collection/class-followers.php | 120 ++++++------ includes/model/class-follower.php | 175 +++++------------- includes/table/class-followers.php | 2 +- templates/user-followers-list.php | 1 - tests/test-class-activitypub-activity.php | 2 +- tests/test-class-db-activitypub-followers.php | 14 +- 8 files changed, 146 insertions(+), 213 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 151ebf1..510ed23 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -555,12 +555,12 @@ class Base_Object { * * @return string The JSON string. * - * @return array An Object built from the JSON string. + * @return \Activitypub\Activity\Base_Object An Object built from the JSON string. */ - public static function from_json( $json ) { - $array = wp_json_decode( $json, true ); + public static function init_from_json( $json ) { + $array = \json_decode( $json, true ); - return self::from_array( $array ); + return self::init_from_array( $array ); } /** @@ -568,9 +568,9 @@ class Base_Object { * * @return string The object array. * - * @return array An Object built from the JSON string. + * @return \Activitypub\Activity\Base_Object An Object built from the JSON string. */ - public static function from_array( $array ) { + public static function init_from_array( $array ) { $object = new static(); foreach ( $array as $key => $value ) { @@ -581,6 +581,29 @@ class Base_Object { return $object; } + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param string $json The JSON string. + */ + public function from_json( $json ) { + $array = \json_decode( $json, true ); + + $this->from_array( $array ); + } + + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param array $array The array. + */ + public function from_array( $array ) { + foreach ( $array as $key => $value ) { + $key = camel_to_snake_case( $key ); + $this->set( $key, $value ); + } + } + /** * Convert Object to an array. * diff --git a/includes/class-scheduler.php b/includes/class-scheduler.php index 271c4b6..d77a132 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -111,12 +111,11 @@ class Scheduler { $meta = get_remote_metadata_by_actor( $follower->get_url(), true ); if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - $follower->set_error( $meta ); + Followers::add_error( $follower->get__id(), $meta ); } else { - $follower->from_meta( $meta ); + $follower->from_array( $meta ); + $follower->update(); } - - $follower->update(); } } @@ -137,8 +136,7 @@ class Scheduler { if ( 5 <= $follower->count_errors() ) { $follower->delete(); } else { - $follower->set_error( $meta ); - $follower->update(); + Followers::add_error( $follower->get__id(), $meta ); } } else { $follower->reset_errors(); diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index c63a28c..bc35a18 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -64,37 +64,7 @@ class Followers { register_post_meta( self::POST_TYPE, - 'preferred_username', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function( $value ) { - return sanitize_user( $value, true ); - }, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'icon', - array( - 'single' => true, - ) - ); - - register_post_meta( - self::POST_TYPE, - 'url', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), - ) - ); - - register_post_meta( - self::POST_TYPE, - 'inbox', + 'activitypub_inbox', array( 'type' => 'string', 'single' => true, @@ -104,17 +74,7 @@ class Followers { register_post_meta( self::POST_TYPE, - '_shared_inbox', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => array( self::class, 'sanitize_url' ), - ) - ); - - register_post_meta( - self::POST_TYPE, - '_errors', + 'activitypub_errors', array( 'type' => 'string', 'single' => false, @@ -130,10 +90,10 @@ class Followers { register_post_meta( self::POST_TYPE, - '_actor', + 'activitypub_user_id', array( 'type' => 'string', - 'single' => true, + 'single' => false, 'sanitize_callback' => function( $value ) { return esc_sql( $value ); }, @@ -197,13 +157,28 @@ class Followers { return $meta; } - $follower = Follower::from_array( $meta ); + $error = null; + + $follower = new Follower(); + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + $follower->set_id( $actor ); + $follower->set_url( $actor ); + $error = $meta; + } else { + $follower->from_array( $meta ); + } + $follower->upsert(); - $meta = get_post_meta( $follower->get__id(), '_user_id' ); + $meta = get_post_meta( $follower->get__id(), 'activitypub_user_id' ); + + if ( $error ) { + self::add_error( $follower->get__id(), $error ); + } if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + add_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -227,7 +202,7 @@ class Followers { return false; } - return delete_post_meta( $follower->get__id(), '_user_id', $user_id ); + return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); } /** @@ -243,7 +218,7 @@ class Followers { $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -254,7 +229,7 @@ class Followers { if ( $post_id ) { $post = get_post( $post_id ); - return Follower::from_custom_post_type( $post ); + return Follower::init_from_cpt( $post ); } return null; @@ -310,16 +285,16 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) { + public static function get_followers( $user_id, $number = null, $page = null, $args = array() ) { $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, - 'offset' => $offset, + 'paged' => $page, 'orderby' => 'ID', 'order' => 'DESC', 'meta_query' => array( array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -330,7 +305,7 @@ class Followers { $items = array(); foreach ( $query->get_posts() as $post ) { - $items[] = Follower::from_custom_post_type( $post ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore } return $items; @@ -343,11 +318,11 @@ class Followers { * * @return array The Term list of Followers. */ - public static function get_all_followers( $user_id = null ) { + public static function get_all_followers() { $args = array( 'meta_query' => array(), ); - return self::get_followers( $user_id, null, null, $args ); + return self::get_followers( null, null, null, $args ); } /** @@ -364,7 +339,7 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -396,11 +371,11 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => '_shared_inbox', + 'key' => 'activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -418,7 +393,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = '_shared_inbox' + AND meta_key = 'activitypub_inbox' AND meta_value IS NOT NULL", $posts ) @@ -458,7 +433,7 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore } return $items; @@ -478,7 +453,7 @@ class Followers { 'posts_per_page' => $number, 'meta_query' => array( array( - 'key' => 'errors', + 'key' => 'activitypub_errors', 'compare' => 'EXISTS', ), ), @@ -488,9 +463,28 @@ class Followers { $items = array(); foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::from_custom_post_type( $follower ); // phpcs:ignore + $items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore } return $items; } + + /** + * Undocumented function + * + * @param [type] $post_id + * @param [type] $error + * @return void + */ + public static function add_error( $post_id, $error ) { + if ( is_string( $error ) ) { + $error_message = $error; + } elseif ( is_wp_error( $error ) ) { + $error_message = $error->get_error_message(); + } else { + $error_message = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + } + + return add_post_meta( $post_id, 'activitypub_errors', $error_message ); + } } diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 5dd7e2a..573696b 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -24,60 +24,13 @@ class Follower extends Actor { */ protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - /** - * The complete Remote-Profile of the Follower - * - * @var array - */ - protected $_shared_inbox; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * The complete Remote-Profile of the Follower - * - * @var array - */ - protected $_actor; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * The latest received error. - * - * This will only temporary and will saved to $this->errors - * - * @var string - */ - protected $_error; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * A list of errors - * - * @var array - */ - protected $_errors; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore - - /** - * Set new Error - * - * @param mixed $error The latest HTTP-Error. - * - * @return void - */ - public function set_error( $error ) { - $this->_errors = array(); - $this->_error = $error; - } - /** * Get the errors. * * @return mixed */ public function get_errors() { - if ( $this->_errors ) { - return $this->_errors; - } - - $this->_errors = get_post_meta( $this->_id, 'errors' ); - return $this->_errors; + return get_post_meta( $this->_id, 'activitypub_errors' ); } /** @@ -86,7 +39,7 @@ class Follower extends Actor { * @return void */ public function reset_errors() { - delete_post_meta( $this->_id, 'errors' ); + delete_post_meta( $this->_id, 'activitypub_errors' ); } /** @@ -95,7 +48,7 @@ class Follower extends Actor { * @return int The number of errors. */ public function count_errors() { - $errors = $this->get__errors(); + $errors = $this->get_errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return count( $errors ); @@ -104,21 +57,13 @@ class Follower extends Actor { return 0; } - public function get_url() { - if ( ! $this->url ) { - return $this->id; - } - - return $this->url; - } - /** * Return the latest error message. * * @return string The error message. */ public function get_latest_error_message() { - $errors = $this->get__errors(); + $errors = $this->get_errors(); if ( is_array( $errors ) && ! empty( $errors ) ) { return reset( $errors ); @@ -142,16 +87,33 @@ class Follower extends Actor { * @return void */ public function save() { + if ( ! $this->get__id() ) { + global $wpdb; + + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $this->get_id() ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + $this->set__id( $post->ID ); + } + } + $args = array( - 'ID' => $this->get__id(), - 'guid' => $this->get_id(), - 'post_title' => $this->get_name(), - 'post_author' => 0, - 'post_type' => Followers::POST_TYPE, - 'post_name' => $this->get_id(), - 'post_content' => $this->get_summary(), - 'post_status' => 'publish', - 'meta_input' => $this->get_post_meta_input(), + 'ID' => $this->get__id(), + 'guid' => esc_url_raw( $this->get_id() ), + 'post_title' => esc_html( $this->get_name() ), + 'post_author' => 0, + 'post_type' => Followers::POST_TYPE, + 'post_name' => esc_url_raw( $this->get_id() ), + 'post_excerpt' => esc_html( $this->get_summary() ) ? $this->get_summary() : '', + 'post_content' => esc_sql( $this->to_json() ), + 'post_status' => 'publish', + 'meta_input' => $this->get_post_meta_input(), ); $post_id = wp_insert_post( $args ); @@ -164,16 +126,17 @@ class Follower extends Actor { * @return void */ public function upsert() { - if ( $this->_id ) { - $this->update(); - } else { - $this->save(); - } + $this->save(); } /** * 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 */ public function delete() { @@ -186,27 +149,8 @@ class Follower extends Actor { * @return void */ protected function get_post_meta_input() { - $attributes = array( 'inbox', '_shared_inbox', 'icon', 'preferred_username', '_actor', 'url' ); - $meta_input = array(); - - foreach ( $attributes as $attribute ) { - if ( $this->get( $attribute ) ) { - $meta_input[ $attribute ] = $this->get( $attribute ); - } - } - - if ( $this->_error ) { - if ( is_string( $this->_error ) ) { - $_error = $this->_error; - } elseif ( is_wp_error( $this->_error ) ) { - $_error = $this->_error->get_error_message(); - } else { - $_error = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); - } - - $meta_input['_errors'] = $_error; - } + $meta_input['activitypub_inbox'] = esc_url_raw( $this->get_shared_inbox() ); return $meta_input; } @@ -231,37 +175,18 @@ class Follower extends Actor { } /** - * Converts an ActivityPub Array to an Follower Object. + * Get the shared inbox, with a fallback to the inbox. * - * @param array $array The ActivityPub Array. - * - * @return Activitypub\Model\Follower The Follower Object. + * @return string|null The URL to the shared inbox, the inbox or null. */ - public static function from_array( $array ) { - $object = parent::from_array( $array ); - $object->set__actor( $array ); - - global $wpdb; - - $post_id = $wpdb->get_var( - $wpdb->prepare( - "SELECT ID FROM $wpdb->posts WHERE guid=%s", - esc_sql( $object->get_id() ) - ) - ); - - if ( $post_id ) { - $post = get_post( $post_id ); - $object->set__id( $post->ID ); + public function get_shared_inbox() { + if ( ! empty( $this->get_endpoints()['sharedInbox'] ) ) { + return $this->get_endpoints()['sharedInbox']; + } elseif ( ! empty( $this->get_inbox() ) ) { + return $this->get_inbox(); } - if ( ! empty( $object->get_endpoints()['sharedInbox'] ) ) { - $object->_shared_inbox = $object->get_endpoints()['sharedInbox']; - } elseif ( ! empty( $object->get_inbox() ) ) { - $object->_shared_inbox = $object->get_inbox(); - } - - return $object; + return null; } /** @@ -271,16 +196,10 @@ class Follower extends Actor { * * @return array Activitypub\Model\Follower */ - public static function from_custom_post_type( $post ) { - $object = new static(); - + public static function init_from_cpt( $post ) { + $object = self::init_from_json( $post->post_content ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); - $object->set_name( $post->post_title ); - $object->set_summary( $post->post_content ); - $object->set_url( get_post_meta( $post->ID, 'url', true ) ); - $object->set_icon( get_post_meta( $post->ID, 'icon', true ) ); - $object->set_preferred_username( get_post_meta( $post->ID, 'preferred_username', true ) ); $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 93d9456..b815df6 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -55,7 +55,7 @@ class Followers extends WP_List_Table { $page_num = $this->get_pagenum(); $per_page = 20; - $followers = FollowerCollection::get_followers( $this->user_id, $per_page, ( $page_num - 1 ) * $per_page ); + $followers = FollowerCollection::get_followers( $this->user_id, $per_page, $page_num ); $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); diff --git a/templates/user-followers-list.php b/templates/user-followers-list.php index 7476192..576b1cf 100644 --- a/templates/user-followers-list.php +++ b/templates/user-followers-list.php @@ -1,6 +1,5 @@

    -

    diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index b25545c..ba9f5a2 100644 --- a/tests/test-class-activitypub-activity.php +++ b/tests/test-class-activitypub-activity.php @@ -37,7 +37,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase { 'content' => 'Hello world!', ); - $object = \Activitypub\Activity\Base_Object::from_array( $test_array ); + $object = \Activitypub\Activity\Base_Object::init_from_array( $test_array ); $this->assertEquals( 'Hello world!', $object->get_content() ); $this->assertEquals( $test_array, $object->to_array() ); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 3634713..559bd06 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -6,42 +6,42 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { 'url' => 'https://example.org/users/username', 'inbox' => 'https://example.org/users/username/inbox', 'name' => 'username', - 'prefferedUsername' => 'username', + 'preferredUsername' => 'username', ), 'jon@example.com' => array( 'id' => 'https://example.com/author/jon', 'url' => 'https://example.com/author/jon', 'inbox' => 'https://example.com/author/jon/inbox', 'name' => 'jon', - 'prefferedUsername' => 'jon', + 'preferredUsername' => 'jon', ), 'doe@example.org' => array( 'id' => 'https://example.org/author/doe', 'url' => 'https://example.org/author/doe', 'inbox' => 'https://example.org/author/doe/inbox', 'name' => 'doe', - 'prefferedUsername' => 'doe', + 'preferredUsername' => 'doe', ), 'sally@example.org' => array( 'id' => 'http://sally.example.org', 'url' => 'http://sally.example.org', 'inbox' => 'http://sally.example.org/inbox', 'name' => 'jon', - 'prefferedUsername' => 'jon', + 'preferredUsername' => 'jon', ), '12345@example.com' => array( 'id' => 'https://12345.example.com', 'url' => 'https://12345.example.com', 'inbox' => 'https://12345.example.com/inbox', 'name' => '12345', - 'prefferedUsername' => '12345', + 'preferredUsername' => '12345', ), 'user2@example.com' => array( 'id' => 'https://user2.example.com', 'url' => 'https://user2.example.com', 'inbox' => 'https://user2.example.com/inbox', 'name' => 'user2', - 'prefferedUsername' => 'user2', + 'preferredUsername' => 'user2', ), ); @@ -223,7 +223,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); for ( $i = 1; $i <= 15; $i++ ) { - add_post_meta( $follower->get__id(), 'errors', 'error ' . $i ); + add_post_meta( $follower->get__id(), 'activitypub_errors', 'error ' . $i ); } $follower = \Activitypub\Collection\Followers::get_follower( 1, 'http://sally.example.org' ); From d4f5ad8ec19ba2307ec8cbd3decfcaeff5daf13b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 6 Jul 2023 16:10:48 +0200 Subject: [PATCH 318/427] use post_meta instead of post_content --- includes/collection/class-followers.php | 17 +++++++++++++++-- includes/model/class-follower.php | 7 ++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index bc35a18..65d8cdb 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -100,6 +100,18 @@ class Followers { ) ); + register_post_meta( + self::POST_TYPE, + 'activitypub_actor_json', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function( $value ) { + return sanitize_text_field( $value ); + }, + ) + ); + do_action( 'activitypub_after_register_post_type' ); } @@ -254,13 +266,14 @@ class Followers { if ( isset( $object['user_id'] ) ) { unset( $object['user_id'] ); - unset( $object['@context'] ); } + unset( $object['@context'] ); + $user = Users::get_by_id( $user_id ); // get inbox - $inbox = $follower->get_inbox(); + $inbox = $follower->get_shared_inbox(); // send "Accept" activity $activity = new Activity(); diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 573696b..0036f8f 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -111,7 +111,6 @@ class Follower extends Actor { 'post_type' => Followers::POST_TYPE, 'post_name' => esc_url_raw( $this->get_id() ), 'post_excerpt' => esc_html( $this->get_summary() ) ? $this->get_summary() : '', - 'post_content' => esc_sql( $this->to_json() ), 'post_status' => 'publish', 'meta_input' => $this->get_post_meta_input(), ); @@ -150,7 +149,8 @@ class Follower extends Actor { */ protected function get_post_meta_input() { $meta_input = array(); - $meta_input['activitypub_inbox'] = esc_url_raw( $this->get_shared_inbox() ); + $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; } @@ -197,7 +197,8 @@ class Follower extends Actor { * @return array Activitypub\Model\Follower */ public static function init_from_cpt( $post ) { - $object = self::init_from_json( $post->post_content ); + $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); + $object = self::init_from_json( $actor_json ); $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_published ) ) ); From 5b712fb7cd7cdedfb7eb0f5bb53d234a4d51e343 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 13:43:12 +0200 Subject: [PATCH 319/427] fix some last "follower" issues --- includes/activity/class-base-object.php | 2 +- includes/class-activity-dispatcher.php | 12 ++++++++++-- includes/collection/class-followers.php | 2 +- includes/table/class-followers.php | 2 +- templates/blog-user-followers-list.php | 8 ++++---- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 510ed23..9c5f52a 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -661,6 +661,6 @@ class Base_Object { public function to_json() { $array = $this->to_array(); - return \wp_json_encode( $array ); + return \wp_json_encode( $array, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); } } diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 5c91b04..51d69b2 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -23,7 +23,7 @@ class Activity_Dispatcher { */ public static function init() { \add_action( 'activitypub_send_activity', array( self::class, 'send_user_activity' ), 10, 2 ); - \add_action( 'activitypub_send_activity', array( self::class, 'send_blog_activity' ), 10, 2 ); + \add_action( 'activitypub_send_activity', array( self::class, 'send_announce_activity' ), 10, 2 ); } /** @@ -74,6 +74,10 @@ class Activity_Dispatcher { // check if a migration is needed before sending new posts Migration::maybe_migrate(); + if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { + return; + } + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } @@ -110,10 +114,14 @@ class Activity_Dispatcher { * * @return void */ - public static function send_blog_announce_activity( WP_Post $wp_post, $type ) { + public static function send_announce_activity( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); + if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { + return; + } + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 65d8cdb..ce62619 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -298,7 +298,7 @@ class Followers { * * @return array The Term list of Followers, the format depends on $output */ - public static function get_followers( $user_id, $number = null, $page = null, $args = array() ) { + public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index b815df6..b27f311 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -62,7 +62,7 @@ class Followers extends WP_List_Table { $this->set_pagination_args( array( 'total_items' => $counter, - 'total_pages' => round( $counter / $per_page ), + 'total_pages' => ceil( $counter / $per_page ), 'per_page' => $per_page, ) ); diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php index 9ce3bd1..f73826f 100644 --- a/templates/blog-user-followers-list.php +++ b/templates/blog-user-followers-list.php @@ -13,17 +13,17 @@

    - + -

    get_user_count() ) ); ?>

    +

    get_user_count() ) ); ?>

    prepare_items(); - $followers_table->display(); + $table->prepare_items(); + $table->display(); ?> From d00b7b54f21ae824fab0d1a6669a043609e46727 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 14:54:28 +0200 Subject: [PATCH 320/427] use esc_sql --- includes/collection/class-followers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index ce62619..44a1ec8 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -107,7 +107,7 @@ class Followers { 'type' => 'string', 'single' => true, 'sanitize_callback' => function( $value ) { - return sanitize_text_field( $value ); + return esc_sql( $value ); }, ) ); From 4b8ffc874ad25ef5a101aa18cf2464d7f814e466 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:02:34 +0200 Subject: [PATCH 321/427] add pager to followers endpoint --- includes/rest/class-followers.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 38b4746..edd1445 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -58,6 +58,8 @@ class Followers { return $user; } + $page = $request->get_param( 'page', 1 ); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -72,15 +74,22 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore - $json->first = $json->partOf; // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore + $json->partOf = get_rest_url_by_path( sprintf( 'users/%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 / 20 ), $json->partOf ); // phpcs:ignore + + if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) >= $page ) ) { // phpcs:ignore + $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore + } + // phpcs:ignore $json->orderedItems = array_map( function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get__id() ) + FollowerCollection::get_followers( $user->get__id(), 20, $page ) ); $response = new WP_REST_Response( $json, 200 ); @@ -99,6 +108,7 @@ class Followers { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $params['user_id'] = array( From 7f3d31c59ea179e796618be4c20489d4c0bdee29 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:09:22 +0200 Subject: [PATCH 322/427] add prev --- includes/rest/class-followers.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index edd1445..9b9ed89 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -80,10 +80,14 @@ class Followers { $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 20 ), $json->partOf ); // phpcs:ignore - if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) >= $page ) ) { // phpcs:ignore + if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) > $page ) ) { // phpcs:ignore $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore } + if ( $page && ( $page > 1 ) ) { // phpcs:ignore + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + } + // phpcs:ignore $json->orderedItems = array_map( function( $item ) { From f3d2243afbb884fd408bf73a74389b3551df6fd0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 15:10:22 +0200 Subject: [PATCH 323/427] use `paged` instead of `offset` --- includes/rest/class-outbox.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 8485647..047ae22 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -61,7 +61,7 @@ class Outbox { $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); - $page = $request->get_param( 'page', 0 ); + $page = $request->get_param( 'page', 1 ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client @@ -78,9 +78,6 @@ class Outbox { $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore - // phpcs:ignore - $json->totalItems = 0; - foreach ( $post_types as $post_type ) { $count_posts = \wp_count_posts( $post_type ); $json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore @@ -93,13 +90,17 @@ class Outbox { $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore } + if ( $page && ( $page > 1 ) ) { // phpcs:ignore + $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore + } + if ( $page ) { $posts = \get_posts( array( 'posts_per_page' => 10, - 'author' => $user_id, - 'offset' => ( $page - 1 ) * 10, - 'post_type' => $post_types, + 'author' => $user_id, + 'paged' => $page, + 'post_type' => $post_types, ) ); @@ -139,6 +140,7 @@ class Outbox { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $params['user_id'] = array( From 9559a089bed80a8163246fddc71b408deb299d01 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 7 Jul 2023 16:45:38 +0200 Subject: [PATCH 324/427] fix sanitization --- includes/collection/class-followers.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 44a1ec8..449dd78 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -107,7 +107,7 @@ class Followers { 'type' => 'string', 'single' => true, 'sanitize_callback' => function( $value ) { - return esc_sql( $value ); + return sanitize_text_field( $value ); }, ) ); @@ -315,9 +315,10 @@ class Followers { $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); + $posts = $query->get_posts(); $items = array(); - foreach ( $query->get_posts() as $post ) { + foreach ( $posts as $post ) { $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore } From a0a1e33dc8cf59cfd02a9c8167b0cdf3aa946295 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:28:45 +0200 Subject: [PATCH 325/427] Fall back to ID id URL is empty --- includes/model/class-follower.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 0036f8f..39018e2 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -33,6 +33,22 @@ class Follower extends Actor { return get_post_meta( $this->_id, 'activitypub_errors' ); } + /** + * 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. + * + * @return string The URL. + */ + public function get_url() { + if ( $this->url ) { + return $this->url; + } + + return $this->id; + } + /** * Reset (delete) all errors. * From 799f4be1d8d8159ed660f7b7e48f023594341d3c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:29:02 +0200 Subject: [PATCH 326/427] check for "single user mode" --- includes/functions.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index c41304d..725d9f6 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -306,11 +306,27 @@ function is_user_disabled( $user_id ) { } } +/** + * Check if the blog is in single-user mode. + * + * @return boolean True if the blog is in single-user mode, false otherwise. + */ +function is_single_user() { + if ( + false === ACTIVITYPUB_DISABLE_BLOG_USER && + true === ACTIVITYPUB_DISABLE_USER + ) { + return true; + } + + return false; +} + if ( ! function_exists( 'get_self_link' ) ) { /** - * Get the correct self URL + * Returns the link for the currently displayed feed. * - * @return boolean + * @return string Correct link for the atom:self element. */ function get_self_link() { $host = wp_parse_url( home_url() ); From fe99fffab69a0bf1b510f4d4881c7570d924b77f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:29:15 +0200 Subject: [PATCH 327/427] use Group type for blog-user --- includes/model/class-blog-user.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 2549fc4..a1f1be3 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -20,7 +20,7 @@ class Blog_User extends User { * * @var string */ - protected $type = 'Person'; + protected $type = 'Group'; public static function from_wp_user( $user_id ) { if ( is_user_disabled( $user_id ) ) { From 69326d027c6b23efc9cb6f371de7189c20a2e252 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:57:06 +0200 Subject: [PATCH 328/427] return blog-user when in single mode --- includes/transformer/class-post.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index b337f68..03fdff4 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -3,8 +3,10 @@ namespace Activitypub\Transformer; use WP_Post; use Activitypub\Collection\Users; +use Activitypub\Collection\Blog_Users; use Activitypub\Activity\Base_Object; +use function Activitypub\is_single_user; use function Activitypub\get_rest_url_by_path; /** @@ -115,7 +117,7 @@ class Post { $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); - $object->set_attributed_to( Users::get_by_id( $wp_post->post_author )->get_url() ); + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( array( @@ -137,6 +139,22 @@ class Post { return $object; } + /** + * Returns the User-URL of the Author of the Post. + * + * If `single_user` mode is enabled, the URL of the Blog-User is returned. + * + * @return string The User-URL. + */ + protected function get_attributed_to() { + if ( is_single_user() ) { + $user = new Blog_User(); + return $user->get_url(); + } + + return Users::get_by_id( $wp_post->post_author )->get_url(); + } + /** * Generates all Image Attachments for a Post. * From 2252b87b1bb4ac32f37ee70ab233ea0bbdd2b671 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 10:58:34 +0200 Subject: [PATCH 329/427] check what activity should be send --- includes/class-activity-dispatcher.php | 63 ++++++++------------------ 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 51d69b2..4e203c8 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -7,6 +7,7 @@ use Activitypub\Collection\Users; use Activitypub\Collection\Followers; use Activitypub\Transformer\Post; +use function Activitypub\is_single_user; use function Activitypub\is_user_disabled; use function Activitypub\safe_remote_post; @@ -22,8 +23,11 @@ class Activity_Dispatcher { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( 'activitypub_send_activity', array( self::class, 'send_user_activity' ), 10, 2 ); - \add_action( 'activitypub_send_activity', array( self::class, 'send_announce_activity' ), 10, 2 ); + // check if a migration is needed before sending new posts + Migration::maybe_migrate(); + + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 ); + \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); } /** @@ -34,10 +38,7 @@ class Activity_Dispatcher { * * @return void */ - public static function send_user_activity( WP_Post $wp_post, $type ) { - // check if a migration is needed before sending new posts - Migration::maybe_migrate(); - + public static function send_activity( WP_Post $wp_post, $type ) { if ( is_user_disabled( $wp_post->post_author ) ) { return; } @@ -48,8 +49,7 @@ class Activity_Dispatcher { $activity->set_type( $type ); $activity->set_object( $object ); - $user_id = $wp_post->post_author; - $follower_inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $wp_post->post_author ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); @@ -63,46 +63,24 @@ class Activity_Dispatcher { } /** - * Send Activities to followers and mentioned users. + * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. * * @param WP_Post $wp_post The ActivityPub Post. * @param string $type The Activity-Type. * * @return void */ - public static function send_blog_activity( WP_Post $wp_post, $type ) { - // check if a migration is needed before sending new posts - Migration::maybe_migrate(); - - if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) { - return; - } - + public static function send_activity_or_announce( WP_Post $wp_post, $type ) { if ( is_user_disabled( Users::BLOG_USER_ID ) ) { return; } - $user = Users::get_by_id( Users::BLOG_USER_ID ); + $wp_post->post_author = Users::BLOG_USER_ID; - $object = Post::transform( $wp_post )->to_object(); - $object->set_attributed_to( $user->get_id() ); - - $activity = new Activity(); - $activity->set_type( $type ); - $activity->set_actor( $user->get_id() ); - $activity->set_object( $object ); - - $user_id = Users::BLOG_USER_ID; - $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); - - $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); - $inboxes = array_unique( $inboxes ); - - $json = $activity->to_json(); - - foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + if ( is_single_user() ) { + self::send_user_activity( $wp_post, $type ); + } else { + self::send_announce_activity( $wp_post, $type ); } } @@ -114,7 +92,7 @@ class Activity_Dispatcher { * * @return void */ - public static function send_announce_activity( WP_Post $wp_post, $type ) { + public static function send_announce( WP_Post $wp_post, $type ) { // check if a migration is needed before sending new posts Migration::maybe_migrate(); @@ -126,19 +104,16 @@ class Activity_Dispatcher { return; } - $user = Users::get_by_id( Users::BLOG_USER_ID ); - $object = Post::transform( $wp_post )->to_object(); $activity = new Activity(); $activity->set_type( 'Announce' ); - $activity->set_actor( $user->get_id() ); + // to pre-fill attributes like "published" and "id" $activity->set_object( $object ); - + // send only the id $activity->set_object( $object->get_id() ); - $user_id = Users::BLOG_USER_ID; - $follower_inboxes = Followers::get_inboxes( $user_id ); + $follower_inboxes = Followers::get_inboxes( $wp_post->post_author ); $mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() ); $inboxes = array_merge( $follower_inboxes, $mentioned_inboxes ); From b4fb214e705f78742ac136ce536816bcc558e970 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:45:41 +0200 Subject: [PATCH 330/427] make CSS more flexible for various content items --- assets/css/activitypub-admin.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 805aaa5..a7744ce 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -34,10 +34,10 @@ .activitypub-settings-tabs-wrapper { display: -ms-inline-grid; - -ms-grid-columns: 1fr 1fr; + -ms-grid-columns: auto auto auto; vertical-align: top; display: inline-grid; - grid-template-columns: 1fr 1fr 1fr; + grid-template-columns: auto auto auto; } .activitypub-settings-tab.active { From 64d2d2995ba8412eba57825e2f964d9061331b92 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:49:43 +0200 Subject: [PATCH 331/427] oops --- includes/transformer/class-post.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 03fdff4..b408484 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -152,7 +152,7 @@ class Post { return $user->get_url(); } - return Users::get_by_id( $wp_post->post_author )->get_url(); + return Users::get_by_id( $this->wp_post->post_author )->get_url(); } /** From 81d0e09f6e5a013231eb2bba9a37c7a8816da7b1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 11:56:46 +0200 Subject: [PATCH 332/427] fix wrong function names --- includes/class-activity-dispatcher.php | 4 ++-- tests/test-class-activitypub-activity-dispatcher.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 4e203c8..9aed87a 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -78,9 +78,9 @@ class Activity_Dispatcher { $wp_post->post_author = Users::BLOG_USER_ID; if ( is_single_user() ) { - self::send_user_activity( $wp_post, $type ); + self::send_activity( $wp_post, $type ); } else { - self::send_announce_activity( $wp_post, $type ); + self::send_announce( $wp_post, $type ); } } diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index f028798..3e17306 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,7 +34,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); + \Activitypub\Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -74,7 +74,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $pre_http_request = new MockAction(); add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); - \Activitypub\Activity_Dispatcher::send_user_activity( get_post( $post ), 'Create' ); + \Activitypub\Activity_Dispatcher::send_activity( get_post( $post ), 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); From 2f5a3214747a52985567a944b25d359e3c5cf36a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 12:12:12 +0200 Subject: [PATCH 333/427] fix missing user_id issue --- includes/class-activity-dispatcher.php | 50 +++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 9aed87a..98904bc 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -30,6 +30,28 @@ class Activity_Dispatcher { \add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 ); } + /** + * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. + * + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $type The Activity-Type. + * + * @return void + */ + public static function send_activity_or_announce( WP_Post $wp_post, $type ) { + if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + return; + } + + $wp_post->post_author = Users::BLOG_USER_ID; + + if ( is_single_user() ) { + self::send_activity( $wp_post, $type ); + } else { + self::send_announce( $wp_post, $type ); + } + } + /** * Send Activities to followers and mentioned users. * @@ -58,34 +80,12 @@ class Activity_Dispatcher { $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $wp_post->post_author ); } } /** - * Send Activities to followers and mentioned users or `Announce` (boost) a blog post. - * - * @param WP_Post $wp_post The ActivityPub Post. - * @param string $type The Activity-Type. - * - * @return void - */ - public static function send_activity_or_announce( WP_Post $wp_post, $type ) { - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { - return; - } - - $wp_post->post_author = Users::BLOG_USER_ID; - - if ( is_single_user() ) { - self::send_activity( $wp_post, $type ); - } else { - self::send_announce( $wp_post, $type ); - } - } - - /** - * Send Activities to followers and mentioned users. + * Send Announces to followers and mentioned users. * * @param WP_Post $wp_post The ActivityPub Post. * @param string $type The Activity-Type. @@ -122,7 +122,7 @@ class Activity_Dispatcher { $json = $activity->to_json(); foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); + safe_remote_post( $inbox, $json, $wp_post->post_author ); } } } From 465a912a706bed79e045dc0c555f477bf73d0c3b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:08:51 +0200 Subject: [PATCH 334/427] fix user settings --- includes/class-webfinger.php | 1 + templates/user-settings.php | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/includes/class-webfinger.php b/includes/class-webfinger.php index 4a3a1a6..9a53ac4 100644 --- a/includes/class-webfinger.php +++ b/includes/class-webfinger.php @@ -2,6 +2,7 @@ namespace Activitypub; use WP_Error; +use Activitypub\Collection\Users; /** * ActivityPub WebFinger Class diff --git a/templates/user-settings.php b/templates/user-settings.php index 24a6484..a98b6ce 100644 --- a/templates/user-settings.php +++ b/templates/user-settings.php @@ -1,3 +1,4 @@ +

    @@ -8,11 +9,11 @@ From be6d8a1792af14a1deec1b977b49450e6403e756 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:59:12 +0200 Subject: [PATCH 335/427] fix activity --- includes/activity/class-activity.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php index 37a469f..bd13cf8 100644 --- a/includes/activity/class-activity.php +++ b/includes/activity/class-activity.php @@ -196,8 +196,8 @@ class Activity extends Base_Object { $this->set( 'updated', $object->get_updated() ); } - if ( $object->attributed_to() && ! $this->get_actor() ) { - $this->set( 'actor', $object->attributed_to() ); + if ( $object->get_attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->get_attributed_to() ); } if ( $object->get_id() && ! $this->get_id() ) { From 0fab95bfffb8d552a10d876b1339021aa67d9755 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 14:59:35 +0200 Subject: [PATCH 336/427] enhance tests to also test announce and blog wide activities --- ...-class-activitypub-activity-dispatcher.php | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 3e17306..c7abf02 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -39,11 +39,15 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $this->assertSame( 2, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); $first_call_args = array_shift( $all_args ); + $this->assertEquals( 'https://example.com/author/jon/inbox', $first_call_args[2] ); $second_call_args = array_shift( $all_args ); $this->assertEquals( 'https://example.org/users/username/inbox', $second_call_args[2] ); + $json = json_decode( $second_call_args[1]['body'] ); + $this->assertEquals( 'Create', $json->type ); + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } @@ -88,6 +92,39 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_announce() { + $followers = array( 'https://example.com/author/jon' ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Users::BLOG_USER_ID, $follower ); + } + + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello', + ) + ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + \Activitypub\Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); + + $all_args = $pre_http_request->get_args(); + $first_call_args = $all_args[0]; + + $this->assertSame( 1, $pre_http_request->get_call_count() ); + + $user = new \Activitypub\Model\Blog_User(); + + $json = json_decode( $first_call_args[1]['body'] ); + $this->assertEquals( 'Announce', $json->type ); + $this->assertEquals( $user->get_url(), $json->actor ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + public function set_up() { parent::set_up(); add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); From 8920c60c61f32e6a412e2d5afd58fd16efa6aec4 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 10 Jul 2023 15:14:37 +0200 Subject: [PATCH 337/427] final fixes and more tests --- includes/functions.php | 6 ++- includes/transformer/class-post.php | 2 +- ...-class-activitypub-activity-dispatcher.php | 43 +++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 725d9f6..0c3dc70 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -312,14 +312,16 @@ function is_user_disabled( $user_id ) { * @return boolean True if the blog is in single-user mode, false otherwise. */ function is_single_user() { + $return = false; + if ( false === ACTIVITYPUB_DISABLE_BLOG_USER && true === ACTIVITYPUB_DISABLE_USER ) { - return true; + $return = true; } - return false; + return apply_filters( 'activitypub_is_single_user', $return ); } if ( ! function_exists( 'get_self_link' ) ) { diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index b408484..7c7d990 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -3,7 +3,7 @@ namespace Activitypub\Transformer; use WP_Post; use Activitypub\Collection\Users; -use Activitypub\Collection\Blog_Users; +use Activitypub\Model\Blog_User; use Activitypub\Activity\Base_Object; use function Activitypub\is_single_user; diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index c7abf02..54ebda0 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -47,6 +47,8 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $json = json_decode( $second_call_args[1]['body'] ); $this->assertEquals( 'Create', $json->type ); + $this->assertEquals( 'http://example.org/?author=1', $json->actor ); + $this->assertEquals( 'http://example.org/?author=1', $json->object->attributedTo ); remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } @@ -125,6 +127,47 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); } + public function test_dispatch_blog_activity() { + $followers = array( 'https://example.com/author/jon' ); + + add_filter( + 'activitypub_is_single_user', + function( $return ) { + return true; + } + ); + + foreach ( $followers as $follower ) { + \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Users::BLOG_USER_ID, $follower ); + } + + $post = \wp_insert_post( + array( + 'post_author' => 1, + 'post_content' => 'hello', + ) + ); + + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + \Activitypub\Activity_Dispatcher::send_activity_or_announce( get_post( $post ), 'Create' ); + + $all_args = $pre_http_request->get_args(); + $first_call_args = $all_args[0]; + + $this->assertSame( 1, $pre_http_request->get_call_count() ); + + $user = new \Activitypub\Model\Blog_User(); + + $json = json_decode( $first_call_args[1]['body'] ); + $this->assertEquals( 'Create', $json->type ); + $this->assertEquals( $user->get_url(), $json->actor ); + $this->assertEquals( $user->get_url(), $json->object->attributedTo ); + + remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 ); + } + public function set_up() { parent::set_up(); add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 ); From d5a389420dd2a1dee5726b13cbc4e1f338aeed94 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 08:53:18 +0200 Subject: [PATCH 338/427] some fixes based on the feedback of @mattwiebe --- includes/collection/class-followers.php | 24 ++++++++++++++++++------ includes/functions.php | 8 ++++---- includes/model/class-blog-user.php | 2 +- includes/rest/class-followers.php | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index 449dd78..af0156e 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -484,11 +484,16 @@ class Followers { } /** - * Undocumented function + * This function is used to store errors that occur when + * sending an ActivityPub message to a Follower. * - * @param [type] $post_id - * @param [type] $error - * @return void + * The error will be stored in the + * post meta. + * + * @param int $post_id The ID of the WordPress Custom-Post-Type. + * @param mixed $error The error message. Can be a string or a WP_Error. + * + * @return int|false The meta ID on success, false on failure. */ public static function add_error( $post_id, $error ) { if ( is_string( $error ) ) { @@ -496,9 +501,16 @@ class Followers { } elseif ( is_wp_error( $error ) ) { $error_message = $error->get_error_message(); } else { - $error_message = __( 'Unknown Error or misconfigured Error-Message', 'activitypub' ); + $error_message = __( + 'Unknown Error or misconfigured Error-Message', + 'activitypub' + ); } - return add_post_meta( $post_id, 'activitypub_errors', $error_message ); + return add_post_meta( + $post_id, + 'activitypub_errors', + $error_message + ); } } diff --git a/includes/functions.php b/includes/functions.php index 0c3dc70..05e14b9 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -294,14 +294,14 @@ function is_user_disabled( $user_id ) { return false; // if the user is any other user, it's enabled if it can publish posts. default: - if ( ! \user_can( $user_id, 'publish_posts' ) ) { - return true; - } - if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { return ACTIVITYPUB_DISABLE_USER; } + if ( ! \user_can( $user_id, 'publish_posts' ) ) { + return true; + } + return false; } } diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a1f1be3..d1413dc 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -83,7 +83,7 @@ class Blog_User extends User { // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); - $host = \str_replace( 'www.', '', $host ); + $host = \preg_replace( '/^www\./i', '', $host ); $host_parts = \explode( '.', $host ); if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 9b9ed89..cdff551 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -6,7 +6,7 @@ use stdClass; use WP_REST_Server; use WP_REST_Response; use Activitypub\Collection\Users as User_Collection; -use Activitypub\Collection\Followers as FollowerCollection; +use Activitypub\Collection\Followers as Follower_Collection; use function Activitypub\get_rest_url_by_path; @@ -74,7 +74,7 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->totalItems = FollowerCollection::count_followers( $user->get__id() ); // phpcs:ignore + $json->totalItems = Follower_Collection::count_followers( $user->get__id() ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); // phpcs:ignore $json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore @@ -93,7 +93,7 @@ class Followers { function( $item ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user->get__id(), 20, $page ) + Follower_Collection::get_followers( $user->get__id(), 20, $page ) ); $response = new WP_REST_Response( $json, 200 ); From 0ab61b644151ca3458e875acecf3dc1fe8174715 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 08:58:50 +0200 Subject: [PATCH 339/427] make `is_user_disabled` filterable --- includes/functions.php | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 05e14b9..15a769e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -281,29 +281,39 @@ function is_activitypub_request() { * @return boolean True if the user is disabled, false otherwise. */ function is_user_disabled( $user_id ) { + $return = false; + switch ( $user_id ) { // if the user is the application user, it's always enabled. case \Activitypub\Collection\Users::APPLICATION_USER_ID: - return false; + $return = false; + break; // if the user is the blog user, it's only enabled in single-user mode. case \Activitypub\Collection\Users::BLOG_USER_ID: if ( defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) { - return ACTIVITYPUB_DISABLE_BLOG_USER; + $return = ACTIVITYPUB_DISABLE_BLOG_USER; + break; } - return false; + $return = false; + break; // if the user is any other user, it's enabled if it can publish posts. default: if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { - return ACTIVITYPUB_DISABLE_USER; + $return = ACTIVITYPUB_DISABLE_USER; + break; } if ( ! \user_can( $user_id, 'publish_posts' ) ) { - return true; + $return = true; + break; } - return false; + $return = false; + break; } + + return apply_filters( 'activitypub_is_user_disabled', $return, $user_id ); } /** From a461ea3b1f9ef72acd41cdd1a066a08671decb60 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:09:37 +0200 Subject: [PATCH 340/427] some refactorings --- includes/collection/class-users.php | 10 ++++++++++ includes/functions.php | 5 +++++ includes/model/class-user.php | 5 +---- includes/rest/class-inbox.php | 2 -- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php index 62b4264..506722b 100644 --- a/includes/collection/class-users.php +++ b/includes/collection/class-users.php @@ -7,6 +7,8 @@ use Activitypub\Model\User; use Activitypub\Model\Blog_User; use Activitypub\Model\Application_User; +use function Activitypub\is_user_disabled; + class Users { /** * The ID of the Blog User @@ -34,6 +36,14 @@ class Users { $user_id = (int) $user_id; } + if ( is_user_disabled( $user_id ) ) { + return new WP_Error( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + if ( self::BLOG_USER_ID === $user_id ) { return Blog_User::from_wp_user( $user_id ); } elseif ( self::APPLICATION_USER_ID === $user_id ) { diff --git a/includes/functions.php b/includes/functions.php index 15a769e..9d2a03e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -299,6 +299,11 @@ function is_user_disabled( $user_id ) { break; // if the user is any other user, it's enabled if it can publish posts. default: + if ( ! \get_user_by( 'id', $user_id ) ) { + $return = true; + break; + } + if ( defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { $return = ACTIVITYPUB_DISABLE_USER; break; diff --git a/includes/model/class-user.php b/includes/model/class-user.php index d641497..23cd013 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -26,10 +26,7 @@ class User extends Actor { protected $type = 'Person'; public static function from_wp_user( $user_id ) { - if ( - is_user_disabled( $user_id ) || - ! get_user_by( 'id', $user_id ) - ) { + if ( is_user_disabled( $user_id ) ) { return new WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index b530a37..98b4e55 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -124,7 +124,6 @@ class Inbox { * @return WP_REST_Response */ public static function user_inbox_post( $request ) { - $user_id = $request->get_param( 'user_id' ); $user = User_Collection::get_by_various( $user_id ); @@ -150,7 +149,6 @@ class Inbox { * @return WP_REST_Response */ public static function shared_inbox_post( $request ) { - $data = $request->get_params(); $type = $request->get_param( 'type' ); $users = self::extract_recipients( $data ); From befd0d4f1e26c64b37a4502909be310f5db98e0c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:21:16 +0200 Subject: [PATCH 341/427] do not persist data in a getter! --- includes/model/class-blog-user.php | 15 ++++++--------- templates/settings.php | 4 ++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index d1413dc..4cf41f9 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -75,19 +75,12 @@ class Blog_User extends User { * @return string The auto-generated Username. */ public static function get_default_username() { - $username = \get_option( 'activitypub_blog_user_identifier' ); - - if ( $username ) { - return $username; - } - // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); $host_parts = \explode( '.', $host ); if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { - \update_option( 'activitypub_blog_user_identifier', $host ); return $host; } @@ -96,7 +89,6 @@ class Blog_User extends User { $blog_title = \sanitize_title( $blog_title ); if ( strlen( $blog_title ) <= 15 ) { - \update_option( 'activitypub_blog_user_identifier', $blog_title ); return $blog_title; } @@ -111,12 +103,17 @@ class Blog_User extends User { // get random item of $default_identifier $default = $default_identifier[ \array_rand( $default_identifier ) ]; - \update_option( 'activitypub_blog_user_identifier', $default ); return $default; } public function get_preferred_username() { + $username = \get_option( 'activitypub_blog_user_identifier' ); + + if ( $username ) { + return $username; + } + return self::get_default_username(); } diff --git a/templates/settings.php b/templates/settings.php index b587b9e..3da7eaf 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,7 +31,7 @@
    - +

    @@ -58,7 +58,7 @@ - +

    From 57bc4214b70912b61bf9dcaee4ce8a74d5f40d1e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 09:28:10 +0200 Subject: [PATCH 342/427] If the Blog is in "single user" mode, return "Person" insted of "Group". --- includes/model/class-blog-user.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index 4cf41f9..a0efcb6 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -5,6 +5,7 @@ use WP_Query; use Activitypub\Signature; use Activitypub\Collection\Users; +use function Activitypub\is_single_user; use function Activitypub\is_user_disabled; class Blog_User extends User { @@ -206,4 +207,19 @@ class Blog_User extends User { public function get_canonical_url() { return \home_url(); } + + /** + * Get the type of the object. + * + * If the Blog is in "single user" mode, return "Person" insted of "Group". + * + * @return string The type of the object. + */ + public function get_type() { + if ( is_single_user() ) { + return 'Person'; + } else { + return $this->type; + } + } } From e0d767ed98adf1ef641e4f2e291ae388646f830c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:26:07 +0200 Subject: [PATCH 343/427] Fix WebFinger endpoint --- includes/rest/class-webfinger.php | 38 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 846ac50..8e651d0 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -14,7 +14,9 @@ use Activitypub\Collection\Users as User_Collection; */ class Webfinger { /** - * Initialize the class, registering WordPress hooks + * Initialize the class, registering WordPress hooks. + * + * @return void */ public static function init() { \add_action( 'rest_api_init', array( self::class, 'register_routes' ) ); @@ -23,7 +25,9 @@ class Webfinger { } /** - * Register routes + * Register routes. + * + * @return void */ public static function register_routes() { \register_rest_route( @@ -41,10 +45,11 @@ class Webfinger { } /** - * Render JRD file + * WebFinger endpoint. * - * @param WP_REST_Request $request - * @return WP_REST_Response + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response The response object. */ public static function webfinger( $request ) { $resource = $request->get_param( 'resource' ); @@ -76,12 +81,16 @@ class Webfinger { * @param array $array the jrd array * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user + * + * @return array the jrd array */ public static function add_user_discovery( $array, $resource, $user ) { + $user = User_Collection::get_by_id( $user->ID ); + $array['links'][] = array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => \get_author_posts_url( $user->ID ), + 'href' => $user->get_url(), ); return $array; @@ -93,17 +102,24 @@ class Webfinger { * @param array $array the jrd array * @param string $resource the WebFinger resource * @param WP_User $user the WordPress user + * + * @return array the jrd array */ public static function add_pseudo_user_discovery( $array, $resource ) { - if ( ! $array ) { - $array = array(); + if ( $array ) { + return $array; } - $profile = self::get_profile( $resource ); - - return array_merge( $array, $profile ); + return self::get_profile( $resource ); } + /** + * Get the WebFinger profile. + * + * @param string $resource the WebFinger resource. + * + * @return array the WebFinger profile. + */ public static function get_profile( $resource ) { $user = User_Collection::get_by_resource( $resource ); From 002d4e7981d00e7dd12be082dae1e3abc0c4b426 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:34:11 +0200 Subject: [PATCH 344/427] refactoring --- includes/class-http.php | 13 +++++++++++++ includes/class-migration.php | 22 +++++++++++++++++++++- includes/model/class-blog-user.php | 26 ++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/includes/class-http.php b/includes/class-http.php index dd19f9b..1157137 100644 --- a/includes/class-http.php +++ b/includes/class-http.php @@ -25,6 +25,12 @@ class Http { $signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); $wp_version = \get_bloginfo( 'version' ); + + /** + * Filter the HTTP headers user agent. + * + * @param string $user_agent The user agent string. + */ $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); $args = array( 'timeout' => 100, @@ -66,7 +72,14 @@ class Http { $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = \get_bloginfo( 'version' ); + + /** + * Filter the HTTP headers user agent. + * + * @param string $user_agent The user agent string. + */ $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $args = array( 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ), 'limit_response_size' => 1048576, diff --git a/includes/class-migration.php b/includes/class-migration.php index 2e5f19c..1ff8b66 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,6 +1,7 @@ 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); @@ -72,6 +87,11 @@ class Migration { } } } + + // set the default username for the Blog User + if ( ! \get_option( 'activitypub_blog_user_identifier' ) ) { + \update_option( 'activitypub_blog_user_identifier', Blog_User::get_default_username() ); + } } /** diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index a0efcb6..feee097 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -71,7 +71,7 @@ class Blog_User extends User { } /** - * Generate and save a default Username. + * Generate a default Username. * * @return string The auto-generated Username. */ @@ -105,9 +105,19 @@ class Blog_User extends User { // get random item of $default_identifier $default = $default_identifier[ \array_rand( $default_identifier ) ]; - return $default; + /** + * Filter the default blog username. + * + * @param string $default The default username. + */ + return apply_filters( 'activitypub_default_blog_username', $default ); } + /** + * Get the preferred User-Name. + * + * @return string The User-Name. + */ public function get_preferred_username() { $username = \get_option( 'activitypub_blog_user_identifier' ); @@ -118,6 +128,11 @@ class Blog_User extends User { return self::get_default_username(); } + /** + * Get the User-Icon. + * + * @return array|null The User-Icon. + */ public function get_icon() { $image = wp_get_attachment_image_src( get_theme_mod( 'custom_logo' ) ); @@ -131,6 +146,11 @@ class Blog_User extends User { return null; } + /** + * Get the User-Header-Image. + * + * @return array|null The User-Header-Image. + */ public function get_header_image() { if ( \has_header_image() ) { return array( @@ -175,6 +195,8 @@ class Blog_User extends User { } /** + * Get the User-Private-Key. + * * @param int $user_id * * @return mixed From ad18edbceab9bdd8a5964774b5115a9aa597b090 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:40:31 +0200 Subject: [PATCH 345/427] fix #358 --- includes/transformer/class-post.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7c7d990..568a0e3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,7 +116,17 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + + $published = \strtotime( $wp_post->post_date_gmt ); + + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + + $updated = \strtotime( $wp_post->post_modified_gmt ); + + if ( $updated > $published ) { + $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + } + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 4a82edcd22106cb73d5dafd839fb9fe6cceab1bf Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:48:04 +0200 Subject: [PATCH 346/427] Revert "fix #358" This reverts commit ad18edbceab9bdd8a5964774b5115a9aa597b090. --- includes/transformer/class-post.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 568a0e3..7c7d990 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,17 +116,7 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - - $published = \strtotime( $wp_post->post_date_gmt ); - - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); - - $updated = \strtotime( $wp_post->post_modified_gmt ); - - if ( $updated > $published ) { - $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); - } - + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 00fbc296b36b246e068603c35f3cbd75539a0400 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 11 Jul 2023 14:48:49 +0200 Subject: [PATCH 347/427] fix #343 --- includes/transformer/class-post.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 7c7d990..568a0e3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -116,7 +116,17 @@ class Post { $object->set_id( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_url( \esc_url( \get_permalink( $wp_post->ID ) ) ); $object->set_type( $this->get_object_type() ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $wp_post->post_date_gmt ) ) ); + + $published = \strtotime( $wp_post->post_date_gmt ); + + $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + + $updated = \strtotime( $wp_post->post_modified_gmt ); + + if ( $updated > $published ) { + $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + } + $object->set_attributed_to( $this->get_attributed_to() ); $object->set_content( $this->get_content() ); $object->set_content_map( From 5ae978a8bcaded05f249a8b3e12eef9be11dde6f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 13 Jul 2023 10:35:15 +0200 Subject: [PATCH 348/427] `user_id` could be an int and meta always returns strings remove strict comparison in this case and add tests to verify the correct behaviour --- includes/collection/class-followers.php | 3 ++- tests/test-class-db-activitypub-followers.php | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index af0156e..f5527f1 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -189,7 +189,8 @@ class Followers { self::add_error( $follower->get__id(), $error ); } - if ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { + // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict + if ( is_array( $meta ) && ! in_array( $user_id, $meta ) ) { add_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 559bd06..2f21767 100644 --- a/tests/test-class-db-activitypub-followers.php +++ b/tests/test-class-db-activitypub-followers.php @@ -244,6 +244,29 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase { $this->assertEquals( 0, count( $followers ) ); } + public function test_add_duplicate_follower() { + $pre_http_request = new MockAction(); + add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 ); + + $follower = 'https://12345.example.com'; + + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + \Activitypub\Collection\Followers::add_follower( 1, $follower ); + + $db_followers = \Activitypub\Collection\Followers::get_followers( 1 ); + + $this->assertContains( $follower, $db_followers ); + + $follower = current( $db_followers ); + $meta = get_post_meta( $follower->get__id(), 'activitypub_user_id' ); + + $this->assertCount( 1, $meta ); + } + public static function http_request_host_is_external( $in, $host ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { From 626616a747371b2590182be6d4b81a6af9de9776 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 14 Jul 2023 11:29:03 +0200 Subject: [PATCH 349/427] always use host as default username --- includes/model/class-blog-user.php | 29 ++--------------------------- 1 file changed, 2 insertions(+), 27 deletions(-) diff --git a/includes/model/class-blog-user.php b/includes/model/class-blog-user.php index feee097..32d167a 100644 --- a/includes/model/class-blog-user.php +++ b/includes/model/class-blog-user.php @@ -79,38 +79,13 @@ class Blog_User extends User { // check if domain host has a subdomain $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); $host = \preg_replace( '/^www\./i', '', $host ); - $host_parts = \explode( '.', $host ); - - if ( \count( $host_parts ) <= 2 && strlen( $host ) <= 15 ) { - return $host; - } - - // check blog title - $blog_title = \get_bloginfo( 'name' ); - $blog_title = \sanitize_title( $blog_title ); - - if ( strlen( $blog_title ) <= 15 ) { - return $blog_title; - } - - $default_identifier = array( - 'feed', - 'all', - 'everyone', - 'authors', - 'follow', - 'posts', - ); - - // get random item of $default_identifier - $default = $default_identifier[ \array_rand( $default_identifier ) ]; /** * Filter the default blog username. * - * @param string $default The default username. + * @param string $host The default username. */ - return apply_filters( 'activitypub_default_blog_username', $default ); + return apply_filters( 'activitypub_default_blog_username', $host ); } /** From ab30fec6edd46dfd24987ca9bc071fd538e1bfcd Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 14 Jul 2023 13:26:28 +0200 Subject: [PATCH 350/427] added php 7.0 because this is the new min version with WP 6.3 --- .github/workflows/phpunit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 4c70a1e..af05e18 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -15,7 +15,7 @@ jobs: options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10 strategy: matrix: - php-versions: ['5.6', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] + php-versions: ['5.6', '7.0', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2'] steps: - name: Checkout uses: actions/checkout@v2 From 0f54ea465ec34a7d28dae0502cf16a806d35276b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 17 Jul 2023 14:37:17 +0200 Subject: [PATCH 351/427] fix CSRF flaw --- includes/class-admin.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 647805a..aae1b36 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -202,10 +202,13 @@ class Admin { } public static function save_user_description( $user_id ) { - if ( isset( $_REQUEST['_apnonce'] ) && ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) ) { + if ( ! isset( $_REQUEST['_apnonce'] ) ) { return false; } - if ( ! current_user_can( 'edit_user', $user_id ) ) { + if ( + ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) || + ! current_user_can( 'edit_user', $user_id ) + ) { return false; } update_user_meta( $user_id, 'activitypub_user_description', sanitize_text_field( $_POST['activitypub-user-description'] ) ); From d7e9d540638412dae751f2e6dff952bb5cdaa618 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 17 Jul 2023 15:25:30 +0200 Subject: [PATCH 352/427] Checks if item (WP_Post) is "public", a supported post type and not password protected. --- includes/class-shortcodes.php | 53 ++++++++++++++++++++--------------- includes/functions.php | 29 +++++++++++++++++++ 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index be34fd7..144b7d9 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -1,11 +1,18 @@ ID ); if ( ! $tags ) { return ''; @@ -58,13 +65,13 @@ class Shortcodes { * @return string The post title. */ public static function title( $atts, $content, $tag ) { - $post_id = get_the_ID(); + $post = get_item(); - if ( ! $post_id ) { + if ( ! $post ) { return ''; } - return \wp_strip_all_tags( \get_the_title( $post_id ), true ); + return \wp_strip_all_tags( \get_the_title( $post->ID ), true ); } @@ -78,9 +85,9 @@ class Shortcodes { * @return string The post excerpt. */ public static function excerpt( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); - if ( ! $post || \post_password_required( $post ) ) { + if ( ! $post ) { return ''; } @@ -184,9 +191,9 @@ class Shortcodes { // prevent inception remove_shortcode( 'ap_content' ); - $post = get_post(); + $post = get_item(); - if ( ! $post || \post_password_required( $post ) ) { + if ( ! $post ) { return ''; } @@ -226,7 +233,7 @@ class Shortcodes { * @return string The post permalink. */ public static function permalink( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -260,7 +267,7 @@ class Shortcodes { * @return string The post shortlink. */ public static function shortlink( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -294,9 +301,9 @@ class Shortcodes { * @return string */ public static function image( $atts, $content, $tag ) { - $post_id = get_the_ID(); + $post = get_item(); - if ( ! $post_id ) { + if ( ! $post ) { return ''; } @@ -318,7 +325,7 @@ class Shortcodes { $size = $atts['type']; } - $image = \get_the_post_thumbnail_url( $post_id, $size ); + $image = \get_the_post_thumbnail_url( $post->ID, $size ); if ( ! $image ) { return ''; @@ -337,13 +344,13 @@ class Shortcodes { * @return string The post categories as hashtags. */ public static function hashcats( $atts, $content, $tag ) { - $post_id = get_the_ID(); + $post = get_item(); - if ( ! $post_id ) { + if ( ! $post ) { return ''; } - $categories = \get_the_category( $post_id ); + $categories = \get_the_category( $post->ID ); if ( ! $categories ) { return ''; @@ -372,7 +379,7 @@ class Shortcodes { * @return string The author name. */ public static function author( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -397,7 +404,7 @@ class Shortcodes { * @return string The author URL. */ public static function authorurl( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -461,7 +468,7 @@ class Shortcodes { * @return string The post date. */ public static function date( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -490,7 +497,7 @@ class Shortcodes { * @return string The post time. */ public static function time( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; @@ -519,7 +526,7 @@ class Shortcodes { * @return string The post date/time. */ public static function datetime( $atts, $content, $tag ) { - $post = get_post(); + $post = get_item(); if ( ! $post ) { return ''; diff --git a/includes/functions.php b/includes/functions.php index 9d2a03e..b213b9d 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -352,3 +352,32 @@ if ( ! function_exists( 'get_self_link' ) ) { } } +/** + * Get a WordPress item to federate. + * + * Checks if item (WP_Post) is "public", a supported post type + * and not password protected. + * + * @return null|WP_Post The WordPress item. + */ +function get_item() { + $post = \get_post(); + + if ( ! $post ) { + return null; + } + + if ( ! \in_array( $post->post_type, \get_post_types_by_support( 'activitypub' ), true ) ) { + return null; + } + + if ( 'publish' !== $post->post_status ) { + return null; + } + + if ( \post_password_required( $post ) ) { + return null; + } + + return $post; +} From 964ceee869d1e21e22d98b503d3871fa9e8ee4ed Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 17 Jul 2023 17:23:13 +0200 Subject: [PATCH 353/427] fix tests --- README.md | 2 +- includes/class-shortcodes.php | 90 ++++++++++----------- includes/functions.php | 10 +-- readme.txt | 2 +- tests/test-class-activitypub-shortcodes.php | 3 +- 5 files changed, 53 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index b8643fe..e43fea6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ActivityPub # -**Contributors:** [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [akirk](https://profiles.wordpress.org/akirk/), [automattic](https://profiles.wordpress.org/automattic/) +**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/) **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 **Tested up to:** 6.2 diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 144b7d9..c21d74d 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -30,13 +30,13 @@ class Shortcodes { * @return string The post tags as hashtags. */ public static function hashtags( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $tags = \get_the_tags( $post->ID ); + $tags = \get_the_tags( $item->ID ); if ( ! $tags ) { return ''; @@ -65,13 +65,13 @@ class Shortcodes { * @return string The post title. */ public static function title( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - return \wp_strip_all_tags( \get_the_title( $post->ID ), true ); + return \wp_strip_all_tags( \get_the_title( $item->ID ), true ); } @@ -85,9 +85,9 @@ class Shortcodes { * @return string The post excerpt. */ public static function excerpt( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } @@ -103,11 +103,11 @@ class Shortcodes { $excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH; } - $excerpt = \get_post_field( 'post_excerpt', $post ); + $excerpt = \get_post_field( 'post_excerpt', $item ); if ( '' === $excerpt ) { - $content = \get_post_field( 'post_content', $post ); + $content = \get_post_field( 'post_content', $item ); // An empty string will make wp_trim_excerpt do stuff we do not want. if ( '' !== $content ) { @@ -188,22 +188,22 @@ class Shortcodes { * @return string The post content. */ public static function content( $atts, $content, $tag ) { - // prevent inception - remove_shortcode( 'ap_content' ); + $item = get_item(); - $post = get_item(); - - if ( ! $post ) { + if ( ! $item ) { return ''; } + // prevent inception + remove_shortcode( 'ap_content' ); + $atts = shortcode_atts( array( 'apply_filters' => 'yes' ), $atts, $tag ); - $content = \get_post_field( 'post_content', $post ); + $content = \get_post_field( 'post_content', $item ); if ( 'yes' === $atts['apply_filters'] ) { $content = \apply_filters( 'the_content', $content ); @@ -233,9 +233,9 @@ class Shortcodes { * @return string The post permalink. */ public static function permalink( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } @@ -248,12 +248,12 @@ class Shortcodes { ); if ( 'url' === $atts['type'] ) { - return \esc_url( \get_permalink( $post->ID ) ); + return \esc_url( \get_permalink( $item->ID ) ); } return \sprintf( '
    %1$s', - \esc_url( \get_permalink( $post->ID ) ) + \esc_url( \get_permalink( $item->ID ) ) ); } @@ -267,9 +267,9 @@ class Shortcodes { * @return string The post shortlink. */ public static function shortlink( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } @@ -282,12 +282,12 @@ class Shortcodes { ); if ( 'url' === $atts['type'] ) { - return \esc_url( \wp_get_shortlink( $post->ID ) ); + return \esc_url( \wp_get_shortlink( $item->ID ) ); } return \sprintf( '%1$s', - \esc_url( \wp_get_shortlink( $post->ID ) ) + \esc_url( \wp_get_shortlink( $item->ID ) ) ); } @@ -301,9 +301,9 @@ class Shortcodes { * @return string */ public static function image( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } @@ -325,7 +325,7 @@ class Shortcodes { $size = $atts['type']; } - $image = \get_the_post_thumbnail_url( $post->ID, $size ); + $image = \get_the_post_thumbnail_url( $item->ID, $size ); if ( ! $image ) { return ''; @@ -344,13 +344,13 @@ class Shortcodes { * @return string The post categories as hashtags. */ public static function hashcats( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $categories = \get_the_category( $post->ID ); + $categories = \get_the_category( $item->ID ); if ( ! $categories ) { return ''; @@ -379,13 +379,13 @@ class Shortcodes { * @return string The author name. */ public static function author( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $name = \get_the_author_meta( 'display_name', $post->post_author ); + $name = \get_the_author_meta( 'display_name', $item->post_author ); if ( ! $name ) { return ''; @@ -404,13 +404,13 @@ class Shortcodes { * @return string The author URL. */ public static function authorurl( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $url = \get_the_author_meta( 'user_url', $post->post_author ); + $url = \get_the_author_meta( 'user_url', $item->post_author ); if ( ! $url ) { return ''; @@ -468,13 +468,13 @@ class Shortcodes { * @return string The post date. */ public static function date( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $datetime = \get_post_datetime( $post ); + $datetime = \get_post_datetime( $item ); $dateformat = \get_option( 'date_format' ); $timeformat = \get_option( 'time_format' ); @@ -497,13 +497,13 @@ class Shortcodes { * @return string The post time. */ public static function time( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $datetime = \get_post_datetime( $post ); + $datetime = \get_post_datetime( $item ); $dateformat = \get_option( 'date_format' ); $timeformat = \get_option( 'time_format' ); @@ -526,13 +526,13 @@ class Shortcodes { * @return string The post date/time. */ public static function datetime( $atts, $content, $tag ) { - $post = get_item(); + $item = get_item(); - if ( ! $post ) { + if ( ! $item ) { return ''; } - $datetime = \get_post_datetime( $post ); + $datetime = \get_post_datetime( $item ); $dateformat = \get_option( 'date_format' ); $timeformat = \get_option( 'time_format' ); diff --git a/includes/functions.php b/includes/functions.php index b213b9d..1a91670 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -367,11 +367,7 @@ function get_item() { return null; } - if ( ! \in_array( $post->post_type, \get_post_types_by_support( 'activitypub' ), true ) ) { - return null; - } - - if ( 'publish' !== $post->post_status ) { + if ( 'publish' !== \get_post_status( $post ) ) { return null; } @@ -379,5 +375,9 @@ function get_item() { return null; } + if ( ! \in_array( \get_post_type( $post ), \get_post_types_by_support( 'activitypub' ), true ) ) { + return null; + } + return $post; } diff --git a/readme.txt b/readme.txt index 8d1bc99..f90ad68 100644 --- a/readme.txt +++ b/readme.txt @@ -1,5 +1,5 @@ === ActivityPub === -Contributors: pfefferle, mediaformat, akirk, automattic +Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 Tested up to: 6.2 diff --git a/tests/test-class-activitypub-shortcodes.php b/tests/test-class-activitypub-shortcodes.php index 5ba69d9..8637a61 100644 --- a/tests/test-class-activitypub-shortcodes.php +++ b/tests/test-class-activitypub-shortcodes.php @@ -39,13 +39,12 @@ class Test_Activitypub_Shortcodes extends WP_UnitTestCase { $post->post_date_gmt = current_time( 'mysql', 1 ); $post->post_title = 'Some title or other'; $post->post_content = 'hallo'; - $post->post_status = 'publish'; $post->comment_status = 'closed'; $post->ping_status = 'closed'; $post->post_name = 'fake-page-' . rand( 1, 99999 ); // append random number to avoid clash $post->post_type = 'page'; - $post->post_password = 'abc'; $post->filter = 'raw'; // important! + $post->post_password = 'abc'; $content = '[ap_content]'; From ab6aefe446b5cfde8d9ebb41f1033ebb62350bea Mon Sep 17 00:00:00 2001 From: Alex Kirk Date: Tue, 18 Jul 2023 06:30:06 +0200 Subject: [PATCH 354/427] Add missing output escaping --- includes/class-mention.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/includes/class-mention.php b/includes/class-mention.php index a072bde..6798833 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -27,8 +27,12 @@ class Mention { public static function the_content( $the_content ) { $protected_tags = array(); $protect = function( $m ) use ( &$protected_tags ) { - $c = count( $protected_tags ); + $c = \rand( 100000, 999999 ); $protect = '!#!#PROTECT' . $c . '#!#!'; + while ( isset( $protected_tags[ $protect ] ) ) { + $c = \rand( 100000, 999999 ); + $protect = '!#!#PROTECT' . $c . '#!#!'; + } $protected_tags[ $protect ] = $m[0]; return $protect; }; @@ -78,8 +82,7 @@ class Mention { if ( ! empty( $metadata['preferredUsername'] ) ) { $username = $metadata['preferredUsername']; } - $username = '@' . $username . ''; - return \sprintf( '%s', $metadata['url'], $username ); + return \sprintf( '@%s', esc_url( $metadata['url'] ), esc_html( $username ) ); } return $result[0]; From bf8acf9f512c8652ce46afae738ff7e2412f9f89 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 18 Jul 2023 08:14:28 +0200 Subject: [PATCH 355/427] use wp_rand and change hashtags too --- includes/class-hashtag.php | 2 +- includes/class-mention.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/class-hashtag.php b/includes/class-hashtag.php index ca855ef..4f5b0f6 100644 --- a/includes/class-hashtag.php +++ b/includes/class-hashtag.php @@ -45,7 +45,7 @@ class Hashtag { public static function the_content( $the_content ) { $protected_tags = array(); $protect = function( $m ) use ( &$protected_tags ) { - $c = count( $protected_tags ); + $c = \wp_rand( 100000, 999999 ); $protect = '!#!#PROTECT' . $c . '#!#!'; $protected_tags[ $protect ] = $m[0]; return $protect; diff --git a/includes/class-mention.php b/includes/class-mention.php index 6798833..63508e8 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -27,10 +27,10 @@ class Mention { public static function the_content( $the_content ) { $protected_tags = array(); $protect = function( $m ) use ( &$protected_tags ) { - $c = \rand( 100000, 999999 ); + $c = \wp_rand( 100000, 999999 ); $protect = '!#!#PROTECT' . $c . '#!#!'; while ( isset( $protected_tags[ $protect ] ) ) { - $c = \rand( 100000, 999999 ); + $c = \wp_rand( 100000, 999999 ); $protect = '!#!#PROTECT' . $c . '#!#!'; } $protected_tags[ $protect ] = $m[0]; From f4c8264e9a41360d7e6492ae348d3ba811fecd3e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 18 Jul 2023 08:20:09 +0200 Subject: [PATCH 356/427] move function to Shortcode class --- includes/class-shortcodes.php | 58 ++++++++++++++++++++++++++--------- includes/functions.php | 30 ------------------ 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index c21d74d..2825e80 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -1,8 +1,6 @@ Date: Tue, 18 Jul 2023 14:36:33 +0200 Subject: [PATCH 357/427] fix predictability and collision --- includes/class-hashtag.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/includes/class-hashtag.php b/includes/class-hashtag.php index 4f5b0f6..1a76577 100644 --- a/includes/class-hashtag.php +++ b/includes/class-hashtag.php @@ -47,6 +47,10 @@ class Hashtag { $protect = function( $m ) use ( &$protected_tags ) { $c = \wp_rand( 100000, 999999 ); $protect = '!#!#PROTECT' . $c . '#!#!'; + while ( isset( $protected_tags[ $protect ] ) ) { + $c = \wp_rand( 100000, 999999 ); + $protect = '!#!#PROTECT' . $c . '#!#!'; + } $protected_tags[ $protect ] = $m[0]; return $protect; }; From 2596713213fc2e736ae2d6535e5f74709e9a514b Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 18 Jul 2023 15:02:27 -0500 Subject: [PATCH 358/427] Lint: now clean --- activitypub.php | 1 + includes/class-admin.php | 8 ++++++-- includes/class-health-check.php | 10 +++++----- includes/functions.php | 6 +++--- includes/table/class-followers.php | 4 ++-- templates/admin-header.php | 3 +++ templates/user-settings.php | 4 +++- 7 files changed, 23 insertions(+), 13 deletions(-) diff --git a/activitypub.php b/activitypub.php index 75d52b1..0abf189 100644 --- a/activitypub.php +++ b/activitypub.php @@ -109,6 +109,7 @@ if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) { * Add plugin settings link */ function plugin_settings_link( $actions ) { + $settings_link = array(); $settings_link[] = \sprintf( '%2s', \menu_page_url( 'activitypub', false ), diff --git a/includes/class-admin.php b/includes/class-admin.php index aae1b36..98cbb0e 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -205,13 +205,17 @@ class Admin { if ( ! isset( $_REQUEST['_apnonce'] ) ) { return false; } + $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) ); if ( - ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-user-description' ) || + ! wp_verify_nonce( $nonce, 'activitypub-user-description' ) || ! current_user_can( 'edit_user', $user_id ) ) { return false; } - update_user_meta( $user_id, 'activitypub_user_description', sanitize_text_field( $_POST['activitypub-user-description'] ) ); + $description = ! empty( $_POST['activitypub-user-description'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub-user-description'] ) ) : false; + if ( $description ) { + update_user_meta( $user_id, 'activitypub_user_description', $description ); + } } public static function enqueue_scripts( $hook_suffix ) { diff --git a/includes/class-health-check.php b/includes/class-health-check.php index f248d90..02ebfcf 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -125,7 +125,7 @@ class Health_Check { \sprintf( // translators: %s: Author URL \__( - '

    Your author URL %s was replaced, this is often done by plugins.

    ', + 'Your author URL %s was replaced, this is often done by plugins.', 'activitypub' ), $author_url @@ -148,7 +148,7 @@ class Health_Check { \sprintf( // 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.

    ', + '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' ), $author_url @@ -165,7 +165,7 @@ class Health_Check { \sprintf( // translators: %s: Author URL \__( - '

    Your author URL %s is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".

    ', + 'Your author URL %s is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".', 'activitypub' ), $author_url @@ -182,7 +182,7 @@ class Health_Check { \sprintf( // 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.

    ', + 'Your author URL %s does not return valid JSON for application/activity+json. Please check if your hosting supports alternate Accept headers.', 'activitypub' ), $author_url @@ -216,7 +216,7 @@ class Health_Check { 'webfinger_url_invalid_response' => \sprintf( // translators: %s: Author URL \__( - '

    Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.

    ', + 'Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.', 'activitypub' ), $url->get_error_data() diff --git a/includes/functions.php b/includes/functions.php index 69acf43..ec20e2c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -256,7 +256,7 @@ function is_activitypub_request() { * is to send an Accept header. */ if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { - $accept = $_SERVER['HTTP_ACCEPT']; + $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); /* * $accept can be a single value, or a comma separated list of values. @@ -347,7 +347,7 @@ if ( ! function_exists( 'get_self_link' ) ) { */ function get_self_link() { $host = wp_parse_url( home_url() ); - - return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . wp_unslash( $_SERVER['REQUEST_URI'] ) ) ) ); + $path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; + return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) ); } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index b27f311..289a194 100644 --- a/includes/table/class-followers.php +++ b/includes/table/class-followers.php @@ -119,8 +119,8 @@ class Followers extends WP_List_Table { if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_apnonce'] ) ) { return false; } - - if ( ! wp_verify_nonce( $_REQUEST['_apnonce'], 'activitypub-followers-list' ) ) { + $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) ); + if ( ! wp_verify_nonce( $nonce, 'activitypub-followers-list' ) ) { return false; } diff --git a/templates/admin-header.php b/templates/admin-header.php index 3b40468..23fb421 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -1,3 +1,6 @@ +

    diff --git a/templates/user-settings.php b/templates/user-settings.php index a98b6ce..8de8c81 100644 --- a/templates/user-settings.php +++ b/templates/user-settings.php @@ -1,4 +1,6 @@ - +

    - or - + get_resource() ); ?> or + get_url() ); ?>

    -

    +

    get_resource() ) ); ?>

    From cc168c7d40c709090831afafaeb8fc7240d03464 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Tue, 18 Jul 2023 15:13:53 -0500 Subject: [PATCH 359/427] more lint nom --- includes/class-health-check.php | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/includes/class-health-check.php b/includes/class-health-check.php index 02ebfcf..18cd48e 100644 --- a/includes/class-health-check.php +++ b/includes/class-health-check.php @@ -204,21 +204,32 @@ class Health_Check { $url = \Activitypub\Webfinger::resolve( $account ); if ( \is_wp_error( $url ) ) { + $allowed = array( 'code' => array() ); + $not_accessible = wp_kses( + // translators: %s: Author URL + \__( + 'Your WebFinger endpoint %s is not accessible. Please check your WordPress setup or permalink structure.', + 'activitypub' + ), + $allowed + ); + $invalid_response = wp_kses( + // translators: %s: Author URL + \__( + 'Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.', + 'activitypub' + ), + $allowed + ); + $health_messages = array( 'webfinger_url_not_accessible' => \sprintf( - // translators: %s: Author URL - \__( - '

    Your WebFinger endpoint %s is not accessible. Please check your WordPress setup or permalink structure.

    ', - 'activitypub' - ), + $not_accessible, $url->get_error_data() ), 'webfinger_url_invalid_response' => \sprintf( // translators: %s: Author URL - \__( - 'Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.', - 'activitypub' - ), + $invalid_response, $url->get_error_data() ), ); From d3b65f255c59edd9ed7a0887826e701d016e96ad Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 19 Jul 2023 16:49:25 -0500 Subject: [PATCH 360/427] Try dotcom's set of sniffs --- composer.json | 7 ++++++- phpcs.xml | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 68cefb9..16e1add 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,9 @@ "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "*", "yoast/phpunit-polyfills": "^2.0", - "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0" + "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", + "sirbrillig/phpcs-variable-analysis": "^2.11", + "phpcsstandards/phpcsextra": "^1.1.0" }, "config": { "allow-plugins": true @@ -36,6 +38,9 @@ "composer install", "bin/install-wp-tests.sh activitypub-test root activitypub-test test-db latest true", "vendor/bin/phpunit" + ], + "lint": [ + "vendor/bin/phpcs" ] } } diff --git a/phpcs.xml b/phpcs.xml index 07a9a39..ad12dbd 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -23,4 +23,50 @@ + + + + + error + + + + + + + + + error + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + warning + + + **/*.asset.php + From be96b19781e3cf38d500352359d04ac477b5f953 Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 19 Jul 2023 19:54:07 -0500 Subject: [PATCH 361/427] use latest wp coding standards --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 16e1add..8cf4ec0 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "phpcompatibility/php-compatibility": "*", "phpcompatibility/phpcompatibility-wp": "*", "squizlabs/php_codesniffer": "3.*", - "wp-coding-standards/wpcs": "*", + "wp-coding-standards/wpcs": "dev-develop", "yoast/phpunit-polyfills": "^2.0", "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0", "sirbrillig/phpcs-variable-analysis": "^2.11", From 3512206d4884e89cf7708be8a5b836388517ceeb Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 19 Jul 2023 20:39:58 -0500 Subject: [PATCH 362/427] phpcbf fixes --- activitypub.php | 8 ++++---- composer.json | 2 +- includes/class-mention.php | 1 - includes/class-shortcodes.php | 1 - includes/rest/class-inbox.php | 4 ++-- includes/transformer/class-post.php | 2 +- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/activitypub.php b/activitypub.php index 0abf189..0de62e3 100644 --- a/activitypub.php +++ b/activitypub.php @@ -64,7 +64,7 @@ function init() { */ spl_autoload_register( function ( $full_class ) { - $base_dir = \dirname( __FILE__ ) . '/includes/'; + $base_dir = __DIR__ . '/includes/'; $base = 'activitypub'; $class = strtolower( $full_class ); @@ -92,14 +92,14 @@ spl_autoload_register( } ); -require_once \dirname( __FILE__ ) . '/includes/functions.php'; +require_once __DIR__ . '/includes/functions.php'; // load NodeInfo endpoints only if blog is public if ( \get_option( 'blog_public', 1 ) ) { Rest\NodeInfo::init(); } -$debug_file = \dirname( __FILE__ ) . '/includes/debug.php'; +$debug_file = __DIR__ . '/includes/debug.php'; if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) { require_once $debug_file; Debug::init(); @@ -150,7 +150,7 @@ function plugin_settings_link( $actions ) { add_action( 'bp_include', function() { - require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; + require_once __DIR__ . '/integration/class-buddypress.php'; Integration\Buddypress::init(); }, 0 diff --git a/composer.json b/composer.json index 8cf4ec0..d34d110 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "vendor/bin/phpunit" ], "lint": [ - "vendor/bin/phpcs" + "vendor/bin/phpcs -n -q" ] } } diff --git a/includes/class-mention.php b/includes/class-mention.php index 63508e8..fda0dd5 100644 --- a/includes/class-mention.php +++ b/includes/class-mention.php @@ -151,6 +151,5 @@ class Mention { } } return $mentions; - } } diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php index 2825e80..7877b64 100644 --- a/includes/class-shortcodes.php +++ b/includes/class-shortcodes.php @@ -70,7 +70,6 @@ class Shortcodes { } return \wp_strip_all_tags( \get_the_title( $item->ID ), true ); - } /** diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index 98b4e55..e4bef21 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -237,7 +237,7 @@ class Inbox { //'type' => 'enum', //'enum' => array( 'Create' ), //'sanitize_callback' => function( $param, $request, $key ) { - // return \strtolower( $param ); + // return \strtolower( $param ); //}, ); @@ -282,7 +282,7 @@ class Inbox { //'type' => 'enum', //'enum' => array( 'Create' ), //'sanitize_callback' => function( $param, $request, $key ) { - // return \strtolower( $param ); + // return \strtolower( $param ); //}, ); diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 568a0e3..69dcf89 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -187,7 +187,7 @@ class Post { // list post thumbnail first if this post has one if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) { $image_ids[] = \get_post_thumbnail_id( $id ); - $max_images--; + --$max_images; } if ( $max_images > 0 ) { From 201ee16f37a599684a126f72eb7bc8163ddffced Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 10:12:59 +0200 Subject: [PATCH 363/427] fix some issues and re-add "ACTIVITYPUB_SINGLE_USER_MODE" const --- activitypub.php | 3 --- includes/class-activitypub.php | 2 +- includes/class-admin.php | 5 ++++- includes/functions.php | 7 +++++++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/activitypub.php b/activitypub.php index 0abf189..472acb7 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,9 +29,6 @@ function init() { \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) ); - \defined( 'ACTIVITYPUB_DISABLE_USER' ) || \define( 'ACTIVITYPUB_DISABLE_USER', false ); - \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) || \define( 'ACTIVITYPUB_DISABLE_BLOG_USER', false ); - \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 507da58..c16fea2 100644 --- a/includes/class-activitypub.php +++ b/includes/class-activitypub.php @@ -83,7 +83,7 @@ class Activitypub { $json_template = false; // check if user can publish posts - if ( \is_author() && ! Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) { + if ( \is_author() && is_wp_error( Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) ) { return $template; } diff --git a/includes/class-admin.php b/includes/class-admin.php index 98cbb0e..3572f98 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -13,9 +13,12 @@ class Admin { public static function init() { \add_action( 'admin_menu', array( self::class, 'admin_menu' ) ); \add_action( 'admin_init', array( self::class, 'register_settings' ) ); - \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); \add_action( 'personal_options_update', array( self::class, 'save_user_description' ) ); \add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); + + if ( ! is_user_disabled( get_current_user_id() ) ) { + \add_action( 'show_user_profile', array( self::class, 'add_profile' ) ); + } } /** diff --git a/includes/functions.php b/includes/functions.php index ec20e2c..42ca74c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -281,6 +281,13 @@ function is_activitypub_request() { * @return boolean True if the user is disabled, false otherwise. */ function is_user_disabled( $user_id ) { + if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) { + if ( ACTIVITYPUB_SINGLE_USER_MODE ) { + \defined( 'ACTIVITYPUB_DISABLE_USER' ) || \define( 'ACTIVITYPUB_DISABLE_USER', true ); + \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) || \define( 'ACTIVITYPUB_DISABLE_BLOG_USER', false ); + } + } + $return = false; switch ( $user_id ) { From c288fbe021a765a255ec6ea7ea5831cdff51e52c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 10:57:14 +0200 Subject: [PATCH 364/427] some more checks if a blog is in single user mode or not --- includes/functions.php | 80 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 42ca74c..4278501 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,6 +1,7 @@ Date: Thu, 20 Jul 2023 13:21:29 +0200 Subject: [PATCH 365/427] Version bump --- README.md | 6 ++++-- activitypub.php | 2 +- readme.txt | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e43fea6..9df04fe 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ **Tags:** OStatus, fediverse, activitypub, activitystream **Requires at least:** 4.7 **Tested up to:** 6.2 -**Stable tag:** 0.17.0 +**Stable tag:** 1.0.0 **Requires PHP:** 5.6 **License:** MIT **License URI:** http://opensource.org/licenses/MIT @@ -114,8 +114,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). -### Next ### +### 1.0.0 ### +* Update: Improved linter (PHPCS) +* Add: Simple caching * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Update: Complete rewrite of the Follower-System based on Taxonomies. * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. diff --git a/activitypub.php b/activitypub.php index 0de62e3..b683493 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: 0.17.0 + * Version: 1.0.0 * Author: Matthias Pfefferle & Automattic * Author URI: https://automattic.com/ * License: MIT diff --git a/readme.txt b/readme.txt index f90ad68..8449b4a 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nur Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 Tested up to: 6.2 -Stable tag: 0.17.0 +Stable tag: 1.0.0 Requires PHP: 5.6 License: MIT License URI: http://opensource.org/licenses/MIT @@ -114,8 +114,10 @@ Where 'blog' is the path to the subdirectory at which your blog resides. Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub). -= Next = += 1.0.0 = +* Update: Improved linter (PHPCS) +* Add: Simple caching * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . * Update: Complete rewrite of the Follower-System based on Taxonomies. * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. From 98143d9a9085a7c048c46f5ba8f295549201a778 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 13:25:28 +0200 Subject: [PATCH 366/427] phpcs:ignores --- includes/functions.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/includes/functions.php b/includes/functions.php index ec20e2c..6446670 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -214,6 +214,7 @@ function get_rest_url_by_path( $path = '' ) { * * @return string The converted string. */ +// phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound function camel_to_snake_case( $string ) { return strtolower( preg_replace( '/(?query_vars['activitypub'] ) ) { return true; From 44a81742aac23086f6ed2b8ad752da300bcde42e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 14:21:32 +0200 Subject: [PATCH 367/427] Add settings to en/disable user types (for .org users) --- includes/class-admin.php | 20 +++++++++++++++++- includes/functions.php | 10 +++++++-- templates/settings.php | 44 +++++++++++++++++++++++++++++++++++++--- templates/welcome.php | 8 ++++---- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 3572f98..228606c 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -148,7 +148,7 @@ class Admin { array( 'type' => 'boolean', 'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ), - 'default' => 0, + 'default' => '0', ) ); \register_setting( @@ -182,6 +182,24 @@ class Admin { }, ) ); + \register_setting( + 'activitypub', + 'activitypub_enable_users', + array( + 'type' => 'boolean', + 'description' => \__( 'Every Author on this Blog (with the publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ), + 'default' => '1', + ) + ); + \register_setting( + 'activitypub', + 'activitypub_enable_blog_user', + array( + 'type' => 'boolean', + 'description' => \__( 'Your Blog becomes an ActivityPub compatible Profile.', 'activitypub' ), + 'default' => '0', + ) + ); } public static function add_settings_help_tab() { diff --git a/includes/functions.php b/includes/functions.php index 4278501..ee67e16 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -347,7 +347,10 @@ function is_user_type_disabled( $type ) { break; } - // @todo check user settings + if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) { + $return = true; + break; + } $return = false; break; @@ -364,7 +367,10 @@ function is_user_type_disabled( $type ) { break; } - // @todo check user settings + if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) { + $return = true; + break; + } $return = false; break; diff --git a/templates/settings.php b/templates/settings.php index 3da7eaf..de74554 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -33,7 +33,7 @@ -

    +

    @@ -58,9 +58,47 @@ +
    + -

    +

    + +

    + +
    + + + + + + +
    + + +

    + +

    +

    + publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ), array( 'code' => array() ) ); ?> +

    +

    + +

    +

    + +

    +
    + + + +

    @@ -222,7 +260,7 @@ -

    +

    diff --git a/templates/welcome.php b/templates/welcome.php index 02e8be3..9233254 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -17,7 +17,7 @@ -

    +

    -

    +

    ID ); @@ -63,7 +63,7 @@ -

    +


    -

    +

    From f734e511f7f125af512cd1811013ade0aef0543c Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 14:53:34 +0200 Subject: [PATCH 368/427] fix tests --- includes/class-activity-dispatcher.php | 2 +- includes/functions.php | 2 +- ...-class-activitypub-activity-dispatcher.php | 23 +++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/includes/class-activity-dispatcher.php b/includes/class-activity-dispatcher.php index 98904bc..f37e709 100644 --- a/includes/class-activity-dispatcher.php +++ b/includes/class-activity-dispatcher.php @@ -39,7 +39,7 @@ class Activity_Dispatcher { * @return void */ public static function send_activity_or_announce( WP_Post $wp_post, $type ) { - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + if ( is_user_type_disabled( 'blog' ) ) { return; } diff --git a/includes/functions.php b/includes/functions.php index ee67e16..80ed03e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -401,7 +401,7 @@ function is_single_user() { $return = true; } - return apply_filters( 'activitypub_is_single_user', $return ); + return $return; } if ( ! function_exists( 'get_self_link' ) ) { diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 54ebda0..b1eb3a0 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -95,6 +95,13 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT } public function test_dispatch_announce() { + add_filter( + 'activitypub_is_user_type_disabled', + function( $value ) { + return false; + } + ); + $followers = array( 'https://example.com/author/jon' ); foreach ( $followers as $follower ) { @@ -131,12 +138,20 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT $followers = array( 'https://example.com/author/jon' ); add_filter( - 'activitypub_is_single_user', - function( $return ) { - return true; - } + 'activitypub_is_user_type_disabled', + function( $value, $type ) { + if ( 'blog' === $type ) { + return false; + } else { + return true; + } + }, + 10, + 2 ); + $this->assertTrue( \Activitypub\is_single_user() ); + foreach ( $followers as $follower ) { \Activitypub\Collection\Followers::add_follower( \Activitypub\Collection\Users::BLOG_USER_ID, $follower ); } From 3b88d5e36c4f245506d1d01a9c74722ae15bfc6a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 15:19:19 +0200 Subject: [PATCH 369/427] update checks --- includes/class-admin.php | 10 +++++----- templates/settings.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/includes/class-admin.php b/includes/class-admin.php index 228606c..2771d76 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -77,10 +77,10 @@ class Admin { * Load user settings page */ public static function followers_list_page() { - if ( ! current_user_can( 'publish_posts' ) ) { - return; + // user has to be able to publish posts + if ( ! is_user_disabled( get_current_user_id() ) ) { + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' ); } - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' ); } /** @@ -166,9 +166,9 @@ class Admin { 'activitypub_blog_user_identifier', array( 'type' => 'string', - 'description' => \esc_html__( 'The Identifier of th Blog-User', 'activitypub' ), + 'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ), 'show_in_rest' => true, - 'default' => 'feed', + 'default' => \Activitypub\Model\Blog_User::get_default_username(), 'sanitize_callback' => function( $value ) { // hack to allow dots in the username $parts = explode( '.', $value ); diff --git a/templates/settings.php b/templates/settings.php index de74554..4270355 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -1,6 +1,6 @@ 'active', From a5bc7628cf7db656aaddaab2d1f76b80c625a38b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 15:53:06 +0200 Subject: [PATCH 370/427] do not hide settings for now --- templates/settings.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/settings.php b/templates/settings.php index 4270355..3d0bccb 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -28,11 +28,11 @@

    +
    + - -

    @@ -60,8 +60,6 @@
    - -

    From dd7daf29da1acae661890795be9fa90bb0481494 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 20 Jul 2023 18:33:24 +0200 Subject: [PATCH 371/427] simpler filter thanks @mattwiebe --- tests/test-class-activitypub-activity-dispatcher.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index b1eb3a0..b7534a0 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -95,12 +95,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT } public function test_dispatch_announce() { - add_filter( - 'activitypub_is_user_type_disabled', - function( $value ) { - return false; - } - ); + add_filter( 'activitypub_is_user_type_disabled', '__return_false' ); $followers = array( 'https://example.com/author/jon' ); From 426ddffba04abcf5e4d01d323341726f26aa0f17 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Jul 2023 14:52:18 +0200 Subject: [PATCH 372/427] First step of the settings redesign thanks @nuriapenya for your help and the nice Screens! --- assets/css/activitypub-admin.css | 24 +- templates/blog-user-followers-list.php | 4 +- templates/settings.php | 480 ++++++++++++------------- templates/welcome.php | 148 ++++---- 4 files changed, 341 insertions(+), 315 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index a7744ce..b6090d8 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -1,4 +1,4 @@ -.activitypub-settings-body { +.activitypub-settings { max-width: 800px; margin: 0 auto; } @@ -151,12 +151,12 @@ summary { background-color: #f6f7f7; } -.activitypub-settings-body +.activitypub-settings input.blog-user-identifier { text-align: right; } -.activitypub-settings-body +.activitypub-settings .header-image { width: 100%; height: 80px; @@ -168,7 +168,7 @@ input.blog-user-identifier { background-size: cover; } -.activitypub-settings-body +.activitypub-settings .logo { height: 80px; width: 80px; @@ -176,3 +176,19 @@ input.blog-user-identifier { top: 40px; left: 40px; } + +.settings_page_activitypub .box { + border: 1px solid #c3c4c7; + background-color: #fff; + padding: 1em 1.5em; + margin-bottom: 1.5em; +} + +.settings_page_activitypub .activitypub-welcome-page .box label { + font-weight: bold; +} + +.settings_page_activitypub .plugin-recommendations { + border-bottom: none; + margin-bottom: 0; +} diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php index f73826f..28e47b8 100644 --- a/templates/blog-user-followers-list.php +++ b/templates/blog-user-followers-list.php @@ -1,6 +1,6 @@ '', @@ -10,7 +10,7 @@ ); ?> -
    +

    diff --git a/templates/settings.php b/templates/settings.php index 3d0bccb..aacc8e1 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -10,7 +10,7 @@ ); ?> -
    +

    -
    - -

    +
    +

    -

    +

    - - - - - - - -
    - - - -

    - -

    -
    - - - -
    - -

    - -

    - - - - - - + + +
    - - -

    -

    + +

    +
    - + +
    -

    +
    +

    -

    +

    - - - - - - - - - - - - - - - - - + + + + + + +
    - - -

    - -

    -

    - -

    -

    - -

    -

    - -

    -

    - -

    - -
    -
      -
    • [ap_title] -
    • -
    • [ap_content] -
    • -
    • [ap_excerpt] -
    • -
    • [ap_permalink] -
    • -
    • [ap_shortlink] - Hum.', 'activitypub' ), 'default' ); ?>
    • -
    • [ap_hashtags] -
    • -
    -

    -
    -
    -

    -
    - - - -

    - %s', 'activitypub' ), - \esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) - ), - 'default' - ); - ?> -

    -
    - - -

    - -

    -

    - -

    -

    - -

    -
    -
    - + + + + + + + + + + + + + + + + + - - - - - - -
    + + +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    + +
    +
      +
    • [ap_title] -
    • +
    • [ap_content] -
    • +
    • [ap_excerpt] -
    • +
    • [ap_permalink] -
    • +
    • [ap_shortlink] - Hum.', 'activitypub' ), 'default' ); ?>
    • +
    • [ap_hashtags] -
    • +
    +

    +
    +
    +

    +
    + + + +

    + %s', 'activitypub' ), + \esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) + ), + 'default' + ); + ?> +

    +
    + + +

    + +

    +

    + +

    +

    + +

    +
    +
    + - true ), 'objects' ); ?> - -
      - - -
    • - name, $support_post_types, true ) ); ?> /> - -
    • - -
    -
    -
    - - -

    - -

    -
    + true ), 'objects' ); ?> + +
      + + +
    • + name, $support_post_types, true ) ); ?> /> + +
    • + +
    +
    +
    + + +

    + +

    +
    - + +
    -

    +
    +

    -

    +

    - - - - - - - -
    - - -

    - Disallowed Comment Keys" list.', 'activitypub' ), - \esc_attr( \admin_url( 'options-discussion.php#disallowed_keys' ) ) - ), - 'default' - ); - ?> -

    -
    - - + + + + + + + +
    + + +

    + Disallowed Comment Keys" list.', 'activitypub' ), + \esc_attr( \admin_url( 'options-discussion.php#disallowed_keys' ) ) + ), + 'default' + ); + ?> +

    +
    + +
    diff --git a/templates/welcome.php b/templates/welcome.php index 9233254..38cdfb8 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -1,6 +1,6 @@ '', @@ -10,82 +10,102 @@ ); ?> -
    -

    +
    +
    +

    -

    +

    ActivityPub protocol, including users of Friendica, Pleroma, Pixelfed and Mastodon.', 'activitypub' ), array( 'strong' => array() ) ); ?>

    +
    - - -

    -

    - %1$s or the URL %2$s. This Blog-User will federate all posts written on your Blog, regardless of the User who posted it. You can customize the Blog-User on the Settings page.', - 'activitypub' - ), - \esc_attr( $blog_user->get_resource() ), - \esc_url_raw( $blog_user->get_url() ), - \esc_url_raw( \admin_url( '/options-general.php?page=activitypub&tab=settings' ) ) - ), - 'default' - ); ?> -

    - +
    +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + + + +

    +
    - - -

    -

    - ID ); - echo wp_kses( - \sprintf( - // translators: - \__( - 'People can also follow you by using your Username %1$s or your Author-URL %2$s. Users who can not access this settings page will find their username on the Edit Profile page.', - 'activitypub' - ), - \esc_attr( $user->get_resource() ), - \esc_url_raw( $user->get_url() ), - \esc_url_raw( \admin_url( 'profile.php#activitypub' ) ) - ), - 'default' - ); ?> -

    - +
    +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + +

    +

    + + + +

    +
    -

    -

    - Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', - 'activitypub' +

    +

    +

    + Site Health to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', + 'activitypub' + ), + \esc_url_raw( admin_url( 'site-health.php' ) ) ), - \esc_url_raw( admin_url( 'site-health.php' ) ) - ), - 'default' - ); - ?> -

    + 'default' + ); + ?> +

    +
    + -
    - -

    - -

    +
    +

    +

    +

    From fe90360bf09cf4750f6bb3e030a030fbabc5aada Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Fri, 21 Jul 2023 14:58:49 +0200 Subject: [PATCH 373/427] fix copy and paste issue --- templates/welcome.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/welcome.php b/templates/welcome.php index 38cdfb8..7ed3f54 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -74,7 +74,7 @@

    - +

    From e8d6d523b0a65d94f6d7dd9fe44364ed983b52ea Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Fri, 21 Jul 2023 10:03:20 -0500 Subject: [PATCH 374/427] update changelog --- readme.txt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/readme.txt b/readme.txt index 8449b4a..1ef8890 100644 --- a/readme.txt +++ b/readme.txt @@ -116,10 +116,11 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu = 1.0.0 = -* Update: Improved linter (PHPCS) -* Add: Simple caching +* Add: blog-wide Account (catchall, like `mydomain.com@mydomain.com`) * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . -* Update: Complete rewrite of the Follower-System based on Taxonomies. +* Update: Complete rewrite of the Follower-System based on Custom Post Types. +* Add: Simple caching +* Update: Improved linter (PHPCS) * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. From 36a139698d7b4ecf4db2b622963d47855c53cc3f Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Jul 2023 13:59:29 +0200 Subject: [PATCH 375/427] update URLs --- includes/help.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/includes/help.php b/includes/help.php index 291162c..3f0c7b9 100644 --- a/includes/help.php +++ b/includes/help.php @@ -71,7 +71,5 @@ \get_current_screen()->set_help_sidebar( '

    ' . \__( 'For more information:', 'activitypub' ) . '

    ' . '

    ' . \__( 'Get support', 'activitypub' ) . '

    ' . - '

    ' . \__( 'Report an issue', 'activitypub' ) . '

    ' . - '
    ' . - '

    ' . \__( 'Donate', 'activitypub' ) . '

    ' + '

    ' . \__( 'Report an issue', 'activitypub' ) . '

    ' ); From 8c650c335655d818521091a517aead4a623e0226 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Jul 2023 14:13:26 +0200 Subject: [PATCH 376/427] some more admin improvements --- assets/css/activitypub-admin.css | 5 +++++ templates/settings.php | 5 ++--- templates/welcome.php | 8 ++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index b6090d8..27db71d 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -188,6 +188,11 @@ input.blog-user-identifier { font-weight: bold; } +.settings_page_activitypub .activitypub-welcome-page input { + font-size: 20px; + width: 95%; +} + .settings_page_activitypub .plugin-recommendations { border-bottom: none; margin-bottom: 0; diff --git a/templates/settings.php b/templates/settings.php index aacc8e1..7826815 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -11,7 +11,8 @@ ?>
    -
    +
    +

    -

    - diff --git a/templates/welcome.php b/templates/welcome.php index 7ed3f54..891bb54 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -27,10 +27,10 @@

    - +

    - +

    @@ -59,10 +59,10 @@

    - +

    - +

    From 0fd80a1c524b12a60ef9c41a820e6d962310050b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 24 Jul 2023 14:16:14 +0200 Subject: [PATCH 377/427] update notice --- assets/css/activitypub-admin.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/activitypub-admin.css b/assets/css/activitypub-admin.css index 27db71d..07aadca 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -6,7 +6,7 @@ .settings_page_activitypub .notice { max-width: 800px; margin: auto; - margin-top: 10px; + margin: 0px auto 30px; } .settings_page_activitypub .wrap { From 921ca0c1c65699880b120e2d29340c6f0108e9b1 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Jul 2023 10:47:59 +0200 Subject: [PATCH 378/427] fix actions --- includes/rest/class-followers.php | 2 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 2 +- includes/rest/class-nodeinfo.php | 16 ++++++++++++++-- includes/rest/class-outbox.php | 2 +- includes/rest/class-users.php | 2 +- includes/rest/class-webfinger.php | 5 +++++ 7 files changed, 24 insertions(+), 7 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index cdff551..03d3c2a 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -63,7 +63,7 @@ class Followers { /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ - \do_action( 'activitypub_outbox_pre' ); + \do_action( 'activitypub_rest_followers_pre' ); $json = new stdClass(); diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index 29e7e07..cd77007 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -56,7 +56,7 @@ class Following { /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ - \do_action( 'activitypub_outbox_pre' ); + \do_action( 'activitypub_rest_following_pre' ); $json = new \stdClass(); diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index e4bef21..f3df5c9 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -85,7 +85,7 @@ class Inbox { /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ - \do_action( 'activitypub_inbox_pre' ); + \do_action( 'activitypub_rest_inbox_pre' ); $json = new \stdClass(); diff --git a/includes/rest/class-nodeinfo.php b/includes/rest/class-nodeinfo.php index 1d9de67..711d4fd 100644 --- a/includes/rest/class-nodeinfo.php +++ b/includes/rest/class-nodeinfo.php @@ -1,6 +1,8 @@ array(), ); - return new \WP_REST_Response( $nodeinfo, 200 ); + return new WP_REST_Response( $nodeinfo, 200 ); } /** @@ -119,6 +126,11 @@ class Nodeinfo { * @return WP_REST_Response */ public static function nodeinfo2( $request ) { + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_rest_nodeinfo2_pre' ); + $nodeinfo = array(); $nodeinfo['version'] = '1.0'; @@ -160,7 +172,7 @@ class Nodeinfo { 'outbound' => array(), ); - return new \WP_REST_Response( $nodeinfo, 200 ); + return new WP_REST_Response( $nodeinfo, 200 ); } /** diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index 047ae22..a0d06a9 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -66,7 +66,7 @@ class Outbox { /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ - \do_action( 'activitypub_outbox_pre' ); + \do_action( 'activitypub_rest_outbox_pre' ); $json = new stdClass(); diff --git a/includes/rest/class-users.php b/includes/rest/class-users.php index b4f85fe..b678043 100644 --- a/includes/rest/class-users.php +++ b/includes/rest/class-users.php @@ -66,7 +66,7 @@ class Users { /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ - \do_action( 'activitypub_outbox_pre' ); + \do_action( 'activitypub_rest_users_pre' ); $user->set_context( Activity::CONTEXT diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php index 8e651d0..4504e0d 100644 --- a/includes/rest/class-webfinger.php +++ b/includes/rest/class-webfinger.php @@ -52,6 +52,11 @@ class Webfinger { * @return WP_REST_Response The response object. */ public static function webfinger( $request ) { + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_rest_webfinger_pre' ); + $resource = $request->get_param( 'resource' ); $response = self::get_profile( $resource ); From 38cd0b973ba97a154320ca61e1ea6e82c410e0a8 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Jul 2023 13:47:49 +0200 Subject: [PATCH 379/427] fix ID --- includes/rest/class-followers.php | 2 +- includes/rest/class-following.php | 2 +- includes/rest/class-inbox.php | 2 +- includes/rest/class-outbox.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index 03d3c2a..a07514e 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -69,7 +69,7 @@ class Followers { $json->{'@context'} = \Activitypub\get_context(); - $json->id = \home_url( \add_query_arg( null, null ) ); + $json->id = get_rest_url_by_path( sprintf( 'users/%d/followers', $user->get__id() ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php index cd77007..f212677 100644 --- a/includes/rest/class-following.php +++ b/includes/rest/class-following.php @@ -62,7 +62,7 @@ class Following { $json->{'@context'} = \Activitypub\get_context(); - $json->id = \home_url( \add_query_arg( null, null ) ); + $json->id = get_rest_url_by_path( sprintf( 'users/%d/following', $user->get__id() ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php index f3df5c9..d45395c 100644 --- a/includes/rest/class-inbox.php +++ b/includes/rest/class-inbox.php @@ -90,7 +90,7 @@ class Inbox { $json = new \stdClass(); $json->{'@context'} = get_context(); - $json->id = \home_url( \add_query_arg( null, null ) ); + $json->id = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->type = 'OrderedCollectionPage'; $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php index a0d06a9..b7ff095 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -71,7 +71,7 @@ class Outbox { $json = new stdClass(); $json->{'@context'} = get_context(); - $json->id = \home_url( \add_query_arg( null, null ) ); + $json->id = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; From 38342889227ff21e53e5e3c3d6d2c9388d043b1d Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 25 Jul 2023 14:34:14 +0200 Subject: [PATCH 380/427] fix issue with API endpoint of WordPress.com --- includes/class-signature.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-signature.php b/includes/class-signature.php index b7c98fc..5ae37a9 100644 --- a/includes/class-signature.php +++ b/includes/class-signature.php @@ -156,7 +156,7 @@ class Signature { public static function verify_http_signature( $request ) { if ( is_object( $request ) ) { // REST Request object // check if route starts with "index.php" - if ( str_starts_with( $request->get_route(), '/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(), '/' ); From 4ed4143d21ec402ac4edd9df4e816748a532288f Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Jul 2023 12:02:18 +0200 Subject: [PATCH 381/427] Post class: declare $attachments property This should avoid PHP notices like this one when running PHP 8.2: PHP Deprecated: Creation of dynamic property Activitypub\Transformer\Post::$attachments is deprecated in /var/www/html/wp-content/plugins/activitypub/includes/transformer/class-post.php on line 249 --- includes/transformer/class-post.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 69dcf89..6f1fed3 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -28,6 +28,13 @@ class Post { */ protected $wp_post; + /** + * Attachment images, used in the content. + * + * @var array + */ + protected $attachments = array(); + /** * The Allowed Tags, used in the content. * From 832660c6af6dd287b8fff9ea84d19afd3757ee68 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Jul 2023 12:03:09 +0200 Subject: [PATCH 382/427] Add changelog --- readme.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/readme.txt b/readme.txt index 1ef8890..1c90107 100644 --- a/readme.txt +++ b/readme.txt @@ -124,6 +124,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. +* Compatibility: avoid PHP notice on sites using PHP 8.2. = 0.17.0 = From b25231a35502e68c34addbede2e9d1c8a9e4c0a7 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Jul 2023 19:16:36 +0200 Subject: [PATCH 383/427] Remove obsolete code https://github.com/Automattic/wordpress-activitypub/pull/370#issuecomment-1652053210 --- includes/transformer/class-post.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php index 6f1fed3..e9453dc 100644 --- a/includes/transformer/class-post.php +++ b/includes/transformer/class-post.php @@ -28,13 +28,6 @@ class Post { */ protected $wp_post; - /** - * Attachment images, used in the content. - * - * @var array - */ - protected $attachments = array(); - /** * The Allowed Tags, used in the content. * @@ -253,8 +246,6 @@ class Post { } } - $this->attachments = $images; - return $images; } From ef369e8ca864ca32d5531255e9c6958ba563eaf4 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 26 Jul 2023 19:38:50 +0200 Subject: [PATCH 384/427] General: update tested WP version for upcoming WordPress 6.3. --- README.md | 16 ++++++++-------- readme.txt | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9df04fe..69079d9 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # ActivityPub # -**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/) -**Tags:** OStatus, fediverse, activitypub, activitystream -**Requires at least:** 4.7 -**Tested up to:** 6.2 -**Stable tag:** 1.0.0 -**Requires PHP:** 5.6 -**License:** MIT -**License URI:** http://opensource.org/licenses/MIT +**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/) +**Tags:** OStatus, fediverse, activitypub, activitystream +**Requires at least:** 4.7 +**Tested up to:** 6.3 +**Stable tag:** 1.0.0 +**Requires PHP:** 5.6 +**License:** MIT +**License URI:** http://opensource.org/licenses/MIT The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. diff --git a/readme.txt b/readme.txt index 1c90107..c61c6bf 100644 --- a/readme.txt +++ b/readme.txt @@ -2,7 +2,7 @@ Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena Tags: OStatus, fediverse, activitypub, activitystream Requires at least: 4.7 -Tested up to: 6.2 +Tested up to: 6.3 Stable tag: 1.0.0 Requires PHP: 5.6 License: MIT @@ -123,7 +123,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu * Update: Improved linter (PHPCS) * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. -* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. * Compatibility: avoid PHP notice on sites using PHP 8.2. = 0.17.0 = From 7ac2533940410fc4e1b84e7de8093512ded0649a Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Wed, 26 Jul 2023 19:57:53 +0200 Subject: [PATCH 385/427] auto generate README.md @jeherve there is a grunt task to auto generate the readme.md from the readme.txt simply run `npm install` and then `grunt` in your terminal. --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 69079d9..5824384 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # ActivityPub # -**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/) -**Tags:** OStatus, fediverse, activitypub, activitystream -**Requires at least:** 4.7 -**Tested up to:** 6.3 -**Stable tag:** 1.0.0 -**Requires PHP:** 5.6 -**License:** MIT -**License URI:** http://opensource.org/licenses/MIT +**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/) +**Tags:** OStatus, fediverse, activitypub, activitystream +**Requires at least:** 4.7 +**Tested up to:** 6.3 +**Stable tag:** 1.0.0 +**Requires PHP:** 5.6 +**License:** MIT +**License URI:** http://opensource.org/licenses/MIT The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format. @@ -116,13 +116,15 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu ### 1.0.0 ### -* Update: Improved linter (PHPCS) -* Add: Simple caching +* Add: blog-wide Account (catchall, like `mydomain.com@mydomain.com`) * Add: Signature Verification: https://docs.joinmastodon.org/spec/security/ . -* Update: Complete rewrite of the Follower-System based on Taxonomies. +* Update: Complete rewrite of the Follower-System based on Custom Post Types. +* Add: Simple caching +* Update: Improved linter (PHPCS) * Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests. -* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. +* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.3. +* Compatibility: avoid PHP notice on sites using PHP 8.2. ### 0.17.0 ### From 5b9dadd6fd7e93f36a1798f653066af9c65292bf Mon Sep 17 00:00:00 2001 From: Matt Wiebe Date: Wed, 26 Jul 2023 15:05:41 -0500 Subject: [PATCH 386/427] Followers Block (#344) Introduces a new Followers block. Proudly display your Fediverse followers to the world! --------- Co-authored-by: Matthias Pfefferle --- .distignore | 1 + activitypub.php | 2 + assets/img/mp.jpg | Bin 0 -> 1323 bytes build/followers/block.json | 57 ++++++ build/followers/index.asset.php | 1 + build/followers/index.js | 3 + build/followers/style-view.css | 1 + build/followers/view.asset.php | 1 + build/followers/view.js | 3 + includes/activity/class-base-object.php | 10 +- includes/class-blocks.php | 90 +++++++++ includes/collection/class-followers.php | 46 +++-- includes/model/class-follower.php | 92 +++++++++ includes/rest/class-followers.php | 36 +++- package.json | 11 ++ src/followers/block.json | 41 ++++ src/followers/edit.js | 68 +++++++ src/followers/followers.js | 105 ++++++++++ src/followers/index.js | 5 + src/followers/pagination-page.js | 18 ++ src/followers/pagination.js | 82 ++++++++ src/followers/pagination.scss | 179 ++++++++++++++++++ src/followers/style.scss | 82 ++++++++ src/followers/view.js | 12 ++ templates/user-followers-list.php | 2 +- tests/test-class-db-activitypub-followers.php | 26 +++ 26 files changed, 948 insertions(+), 26 deletions(-) create mode 100644 assets/img/mp.jpg create mode 100644 build/followers/block.json create mode 100644 build/followers/index.asset.php create mode 100644 build/followers/index.js create mode 100644 build/followers/style-view.css create mode 100644 build/followers/view.asset.php create mode 100644 build/followers/view.js create mode 100644 includes/class-blocks.php create mode 100644 src/followers/block.json create mode 100644 src/followers/edit.js create mode 100644 src/followers/followers.js create mode 100644 src/followers/index.js create mode 100644 src/followers/pagination-page.js create mode 100644 src/followers/pagination.js create mode 100644 src/followers/pagination.scss create mode 100644 src/followers/style.scss create mode 100644 src/followers/view.js diff --git a/.distignore b/.distignore index 782f2e8..55d2ee9 100644 --- a/.distignore +++ b/.distignore @@ -36,3 +36,4 @@ phpunit.xml.dist tests node_modules vendor +src diff --git a/activitypub.php b/activitypub.php index 28aa99c..a8d3a94 100644 --- a/activitypub.php +++ b/activitypub.php @@ -32,6 +32,7 @@ function init() { \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); + \define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); Migration::init(); Activitypub::init(); @@ -50,6 +51,7 @@ function init() { Admin::init(); Hashtag::init(); Shortcodes::init(); + Blocks::init(); Mention::init(); Health_Check::init(); Scheduler::init(); diff --git a/assets/img/mp.jpg b/assets/img/mp.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0356f911c6b01894f0983ecb5f66b01401f84cdc GIT binary patch literal 1323 zcmex=_1P|rX?qqI0P zFI~aY%U!`Mz|~!$%*;qrN1?DZF(6Oj-S5fuR$!pIEN!@|nR z%E~Fi%grl7GWdUhL6CzXfFXdHQHg;`kdaxC@&6G9d7vj*8Nq-73K*GyZe(NU;N;>4 zD%dK(z{JSR%*4VBay3wOEl{3;MUYiU(a@1iI53f2sZhkIapFP_Wv7h?MT0JWP%%y_ zYU1P)6PJ*bQdLve(9|+9H8Z!cv~qTFb#wRd^a>6M4GWKmj7m;PO-s+n%qlJ^Ei136 ztZHs)ZENr7?3y%r%G7DoXUv?nXz`Mz%a*TLxoXqqEnBy3-?4Mop~FXx9y@;G&P778mFHFAhJO}QuuFG%jI8)?h-XAJ6@h5YR)O`c_i3@nTG!`{*0kPR0$*NuTT_rc4C@4(PRNs(4 z-F|H?^N;W|&CPM=AMnc@UKe>ZwNk;?%W(TM`_~QcZ2!7Hc92~v->NFYu)no}zfV6n z;A7fSlN6E9WlYv@8S9r;<@eVV@-?QbE?v{)>*acjY0giF{|tiPzlc6s^5n@UUj|SJ zfWk!7bncSt-wSR#v_6~Kq}2E10_$1k!{6sA?-RS0x#Mf4bgSbl-)i=DJGsm%rX~Fn zdw0vsTqeozpJCHp_Rrtf%5(oH;^%o;calO0eFJIcG!SE;3UMfn1dD=(y z!*})L!zU(-mG_>X&vDHExA%qr4E9wY|JDO7k=bWrV~Pw}aRuXt+(Bjq$cJdsno z^knN4)(JHiz54=p%P(KcQ1kQ0#UEF$Pn#SOdp6ffWR|+dscL~NDF*EebpjcOo(J!| zk`l9a)0=+fAGcTjVgJ0|ZhhvDzzl^Cvpm?1&%CQ=-u|BTrkM36MTsPB0ch;RW{QLApz2KkK=U3mV(~j!=qxMPgjIeU^SL^z3Ss$|) H{@(-u!z~QS literal 0 HcmV?d00001 diff --git a/build/followers/block.json b/build/followers/block.json new file mode 100644 index 0000000..c7015d3 --- /dev/null +++ b/build/followers/block.json @@ -0,0 +1,57 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "activitypub/followers", + "apiVersion": 3, + "version": "1.0.0", + "title": "Fediverse Followers", + "category": "widgets", + "description": "Display your followers from the Fediverse on your website.", + "textdomain": "activitypub", + "icon": "groups", + "supports": { + "html": false + }, + "attributes": { + "title": { + "type": "string", + "default": "Fediverse Followers" + }, + "selectedUser": { + "type": "string", + "default": "site" + }, + "per_page": { + "type": "number", + "default": 10 + }, + "order": { + "type": "string", + "default": "desc", + "enum": [ + "asc", + "desc" + ] + } + }, + "styles": [ + { + "name": "default", + "label": "No Lines", + "isDefault": true + }, + { + "name": "with-lines", + "label": "Lines" + }, + { + "name": "compact", + "label": "Compact" + } + ], + "editorScript": "file:./index.js", + "viewScript": "file:./view.js", + "style": [ + "file:./style-view.css", + "wp-block-query-pagination" + ] +} \ No newline at end of file diff --git a/build/followers/index.asset.php b/build/followers/index.asset.php new file mode 100644 index 0000000..245278a --- /dev/null +++ b/build/followers/index.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '2879986bf189fb73965e'); diff --git a/build/followers/index.js b/build/followers/index.js new file mode 100644 index 0000000..4acf0fe --- /dev/null +++ b/build/followers/index.js @@ -0,0 +1,3 @@ +(()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,r=window.wp.primitives,n=(0,t.createElement)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(r.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"}));function l(){return l=Object.assign?Object.assign.bind():function(e){for(var t=1;t{e.preventDefault(),!a&&l(n)}},r)}const g={outlined:"outlined",minimal:"minimal"};function y(e){let{compact:a,nextLabel:r,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=g.outlined}=e;const p=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(n,Math.ceil(c/o)),u=w()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(d,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(d,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),r&&(0,t.createElement)(d,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":r,className:"wp-block-query-pagination-next block-editor-block-list__block"},r))}const{namespace:f}=window._activityPubOptions;function h(e){let{selectedUser:a,per_page:r,order:n,title:l,page:o,setPage:i,className:c=""}=e;const u="site"===a?0:a,[b,w]=(0,p.useState)([]),[d,g]=(0,p.useState)(0),[h,E]=(0,p.useState)(0),[_,x]=function(){const[e,t]=(0,p.useState)(1);return[e,t]}(),S=o||_,C=i||x,N=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */ +(0,s.__)(" Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),O=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */ +(0,s.__)("More ","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})});return(0,p.useEffect)((()=>{const e=function(e,t,a,r){const n=`/${f}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,v.addQueryArgs)(n,l)}(u,r,n,S);m()({path:e}).then((e=>{g(Math.ceil(e.totalItems/r)),E(e.totalItems),w(e.orderedItems)})).catch((e=>console.error(e)))}),[u,r,n,S]),(0,t.createElement)("div",{className:"activitypub-follower-block "+c},(0,t.createElement)("h3",null,l),(0,t.createElement)("ul",null,b&&b.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(k,e))))),d>1&&(0,t.createElement)(y,{page:S,perPage:r,total:h,pageClick:C,nextLabel:O,prevLabel:N,compact:"is-style-compact"===c}))}function k(e){let{name:a,icon:r,url:n,preferredUsername:l}=e;const i=`@${l}`;return(0,t.createElement)(o.ExternalLink,{className:"activitypub-link",href:n,title:i,onClick:e=>e.preventDefault()},(0,t.createElement)("img",{width:"40",height:"40",src:r.url,class:"avatar activitypub-avatar"}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},i)))}(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:r}=e;const{order:n,per_page:p,selectedUser:u,className:m}=a,v=(0,c.useBlockProps)(),[b,w]=(0,t.useState)(1),d=[{label:(0,s.__)("New to old","activitypub"),value:"desc"},{label:(0,s.__)("Old to new","activitypub"),value:"asc"}],g=(0,i.useSelect)((e=>e("core").getUsers({who:"authors"}))),y=(0,t.useMemo)((()=>{if(!g)return[];const e=[{label:(0,s.__)("Whole Site","activitypub"),value:"site"}];return g.reduce(((e,t)=>(e.push({label:t.name,value:t.id}),e)),e)}),[g]),f=e=>t=>{w(1),r({[e]:t})};return(0,t.createElement)("div",v,(0,t.createElement)(c.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,t.createElement)(o.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:u,options:y,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,s.__)("Sort","activitypub"),value:n,options:d,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,s.__)("Number of Followers","activitypub"),value:p,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(h,l({},a,{page:b,setPage:w})))},save:()=>null,icon:n})})()})(); \ No newline at end of file diff --git a/build/followers/style-view.css b/build/followers/style-view.css new file mode 100644 index 0000000..824879e --- /dev/null +++ b/build/followers/style-view.css @@ -0,0 +1 @@ +.activitypub-follower-block.is-style-compact .activitypub-handle,.activitypub-follower-block.is-style-compact .sep{display:none}.activitypub-follower-block.is-style-with-lines ul li{border-bottom:.5px solid;margin-bottom:.5rem;padding-bottom:.5rem}.activitypub-follower-block.is-style-with-lines ul li:last-child{border-bottom:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle,.activitypub-follower-block.is-style-with-lines .activitypub-name{text-decoration:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle:hover,.activitypub-follower-block.is-style-with-lines .activitypub-name:hover{text-decoration:underline}.activitypub-follower-block ul{margin:0!important;padding:0!important}.activitypub-follower-block li{display:flex;margin-bottom:1rem}.activitypub-follower-block img{border-radius:50%;height:40px;margin-right:var(--wp--preset--spacing--20,.5rem);width:40px}.activitypub-follower-block .activitypub-link{align-items:center;color:inherit!important;display:flex;flex-flow:row nowrap;max-width:100%;text-decoration:none!important}.activitypub-follower-block .activitypub-handle,.activitypub-follower-block .activitypub-name{text-decoration:underline;text-decoration-thickness:.8px;text-underline-position:under}.activitypub-follower-block .activitypub-handle:hover,.activitypub-follower-block .activitypub-name:hover{text-decoration:none}.activitypub-follower-block .activitypub-name{font-size:var(--wp--preset--font-size--normal,16px)}.activitypub-follower-block .activitypub-actor{font-size:var(--wp--preset--font-size--small,13px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follower-block .sep{padding:0 .2rem}.activitypub-follower-block .wp-block-query-pagination{margin-top:1.5rem}.activitypub-follower-block .activitypub-pager{cursor:default}.activitypub-follower-block .activitypub-pager.current{opacity:.33}.activitypub-follower-block .page-numbers{padding:0 .2rem}.activitypub-follower-block .page-numbers.current{font-weight:700;opacity:1} diff --git a/build/followers/view.asset.php b/build/followers/view.asset.php new file mode 100644 index 0000000..5c497bd --- /dev/null +++ b/build/followers/view.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'aa55e14b87c4b4e1c1a3'); diff --git a/build/followers/view.js b/build/followers/view.js new file mode 100644 index 0000000..f88f890 --- /dev/null +++ b/build/followers/view.js @@ -0,0 +1,3 @@ +(()=>{var e,t={142:(e,t,a)=>{"use strict";const r=window.wp.element,n=window.React,l=window.wp.apiFetch;var i=a.n(l);const o=window.wp.url,c=window.wp.i18n;var s=a(184),p=a.n(s);function u(e){let{active:t,children:a,page:n,pageClick:l,className:i}=e;const o=p()("wp-block activitypub-pager",i,{current:t});return(0,r.createElement)("a",{className:o,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const m={outlined:"outlined",minimal:"minimal"};function v(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:i,prevLabel:o,total:c,variant:s=m.outlined}=e;const v=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(n,Math.ceil(c/i)),b=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:b},o&&(0,r.createElement)(u,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},v.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(u,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/i),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function f(e){let{selectedUser:t,per_page:a,order:l,title:s,page:p,setPage:u,className:m=""}=e;const b="site"===t?0:t,[f,w]=(0,n.useState)([]),[y,k]=(0,n.useState)(0),[h,E]=(0,n.useState)(0),[x,_]=function(){const[e,t]=(0,n.useState)(1);return[e,t]}(),O=p||x,N=u||_,S=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */ +(0,c.__)(" Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),C=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */ +(0,c.__)("More ","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})});return(0,n.useEffect)((()=>{const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(b,a,l,O);i()({path:e}).then((e=>{k(Math.ceil(e.totalItems/a)),E(e.totalItems),w(e.orderedItems)})).catch((e=>console.error(e)))}),[b,a,l,O]),(0,r.createElement)("div",{className:"activitypub-follower-block "+m},(0,r.createElement)("h3",null,s),(0,r.createElement)("ul",null,f&&f.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,e))))),y>1&&(0,r.createElement)(v,{page:O,perPage:a,total:h,pageClick:N,nextLabel:C,prevLabel:S,compact:"is-style-compact"===m}))}function g(e){let{name:t,icon:a,url:n,preferredUsername:l}=e;const i=`@${l}`;return(0,r.createElement)(b.ExternalLink,{className:"activitypub-link",href:n,title:i,onClick:e=>e.preventDefault()},(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar"}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},i)))}const w=window.wp.domReady;a.n(w)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(f,t),e)}))}))},184:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e=[],t=0;t{if(!a){var i=1/0;for(p=0;p=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(o=!1,l0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={638:0,962:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[i,o,c]=a,s=0;if(i.some((t=>0!==e[t]))){for(n in o)r.o(o,n)&&(r.m[n]=o[n]);if(c)var p=c(r)}for(t&&t(a);sr(142)));n=r.O(n)})(); \ No newline at end of file diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php index 9c5f52a..8ed6699 100644 --- a/includes/activity/class-base-object.php +++ b/includes/activity/class-base-object.php @@ -8,6 +8,7 @@ namespace Activitypub\Activity; use WP_Error; +use ReflectionClass; use function Activitypub\camel_to_snake_case; use function Activitypub\snake_to_camel_case; @@ -448,9 +449,10 @@ class Base_Object { $var = \strtolower( \substr( $method, 4 ) ); if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - if ( $this->has( $var ) ) { - return $this->get( $var ); + if ( ! $this->has( $var ) ) { + return new WP_Error( 'invalid_key', 'Invalid key' ); } + return $this->$var; } @@ -493,7 +495,7 @@ class Base_Object { return new WP_Error( 'invalid_key', 'Invalid key' ); } - return $this->$key; + return call_user_func( array( $this, 'get_' . $key ) ); } /** @@ -644,7 +646,7 @@ class Base_Object { $array = array_merge( array( '@context' => $context ), $array ); } - $class = new \ReflectionClass( $this ); + $class = new ReflectionClass( $this ); $class = strtolower( $class->getShortName() ); $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); diff --git a/includes/class-blocks.php b/includes/class-blocks.php new file mode 100644 index 0000000..ffc91a5 --- /dev/null +++ b/includes/class-blocks.php @@ -0,0 +1,90 @@ + ACTIVITYPUB_REST_NAMESPACE, + ); + $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) ); + \wp_add_inline_script( $handle, $js, 'before' ); + } + + public static function register_blocks() { + \register_block_type_from_metadata( + ACTIVITYPUB_PLUGIN_DIR . '/build/followers', + array( + 'render_callback' => array( self::class, 'render_follower_block' ), + ) + ); + } + + private static function get_user_id( $user_string ) { + if ( is_numeric( $user_string ) ) { + return absint( $user_string ); + } + // any other non-numeric falls back to 0, including the `site` string used in the UI + return 0; + } + + public static function render_follower_block( $attrs, $content, $block ) { + $followee_user_id = self::get_user_id( $attrs['selectedUser'] ); + $per_page = absint( $attrs['per_page'] ); + $followers = Followers::get_followers( $followee_user_id, $per_page ); + $title = $attrs['title']; + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'aria-label' => __( 'Fediverse Followers', 'activitypub' ), + 'class' => 'activitypub-follower-block', + 'data-attrs' => wp_json_encode( $attrs ), + ) + ); + + $html = '

    '; + if ( $title ) { + $html .= '

    ' . $title . '

    '; + } + $html .= '
      '; + foreach ( $followers as $follower ) { + $html .= '
    • ' . self::render_follower( $follower ) . '
    • '; + } + // We are only pagination on the JS side. Could be revisited but we gotta ship! + $html .= '
    '; + return $html; + } + + public static function render_follower( $follower ) { + $external_svg = ''; + $template = + ' + + + %s + / + @%s + + %s + '; + + $data = $follower->to_array(); + + return sprintf( + $template, + esc_url( $data['url'] ), + esc_attr( $data['name'] ), + esc_attr( $data['icon']['url'] ), + esc_html( $data['name'] ), + esc_html( $data['preferredUsername'] ), + $external_svg + ); + } +} diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php index f5527f1..00a4718 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -292,14 +292,30 @@ class Followers { /** * Get the Followers of a given user * - * @param int $user_id The ID of the WordPress User - * @param string $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT - * @param int $number Limts the result - * @param int $offset Offset - * - * @return array The Term list of Followers, the format depends on $output + * @param int $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * @return array List of `Follower` objects. */ public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { + $data = self::get_followers_with_count( $user_id, $number, $page, $args ); + return $data['followers']; + } + + /** + * Get the Followers of a given user, along with a total count for pagination purposes. + * + * @param int $user_id The ID of the WordPress User. + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return array + * followers List of `Follower` objects. + * total Total number of followers. + */ + public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) { $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, @@ -314,16 +330,16 @@ class Followers { ), ); - $args = wp_parse_args( $args, $defaults ); + $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); - $posts = $query->get_posts(); - $items = array(); - - foreach ( $posts as $post ) { - $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore - } - - return $items; + $total = $query->found_posts; + $followers = array_map( + function( $post ) { + return Follower::init_from_cpt( $post ); + }, + $query->get_posts() + ); + return compact( 'followers', 'total' ); } /** diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index 39018e2..0c5e9b1 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -171,6 +171,55 @@ class Follower extends Actor { return $meta_input; } + /** + * Get the icon. + * + * Sets a fallback to better handle API and HTML outputs. + * + * @return array The icon. + */ + public function get_icon() { + if ( isset( $this->icon['url'] ) ) { + return $this->icon; + } + + return array( + 'type' => 'Image', + 'mediaType' => 'image/jpeg', + 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + ); + } + + /** + * Get Name. + * + * Tries to extract a name from the URL or ID if not set. + * + * @return string The name. + */ + public function get_name() { + if ( isset( $this->name ) ) { + return $this->name; + } + + return $this->extract_name_from_uri(); + } + + /** + * The preferred Username. + * + * Tries to extract a name from the URL or ID if not set. + * + * @return string The preferred Username. + */ + public function get_preferred_username() { + if ( isset( $this->name ) ) { + return $this->name; + } + + return $this->extract_name_from_uri(); + } + /** * Get the Icon URL (Avatar) * @@ -222,4 +271,47 @@ class Follower extends Actor { return $object; } + + /** + * Infer a shortname from the Actor ID or URL. Used only for fallbacks, + * we will try to use what's supplied. + * + * @return string Hopefully the name of the Follower. + */ + protected function extract_name_from_uri() { + // prefer the URL, but fall back to the ID. + if ( $this->url ) { + $name = $this->url; + } else { + $name = $this->id; + } + + if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) { + $name = \rtrim( $name, '/' ); + $path = \wp_parse_url( $name, PHP_URL_PATH ); + + if ( $path ) { + if ( \strpos( $name, '@' ) !== false ) { + // expected: https://example.com/@user (default URL pattern) + $name = \preg_replace( '|^/@?|', '', $path ); + } else { + // expected: https://example.com/users/user (default ID pattern) + $parts = \explode( '/', $path ); + $name = \array_pop( $parts ); + } + } + } elseif ( + \is_email( $name ) || + \strpos( $name, 'acct' ) === 0 || + \strpos( $name, '@' ) === 0 + ) { + // expected: user@example.com or acct:user@example (WebFinger) + $name = \ltrim( $name, '@' ); + $name = \ltrim( $name, 'acct:' ); + $parts = \explode( '@', $name ); + $name = $parts[0]; + } + + return $name; + } } diff --git a/includes/rest/class-followers.php b/includes/rest/class-followers.php index a07514e..c125fcd 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -58,13 +58,17 @@ class Followers { return $user; } - $page = $request->get_param( 'page', 1 ); + $order = $request->get_param( 'order' ); + $per_page = (int) $request->get_param( 'per_page' ); + $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 */ \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(); $json->{'@context'} = \Activitypub\get_context(); @@ -74,13 +78,13 @@ class Followers { $json->actor = $user->get_id(); $json->type = 'OrderedCollectionPage'; - $json->totalItems = Follower_Collection::count_followers( $user->get__id() ); // phpcs:ignore + $json->totalItems = $data['total']; // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%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 / 20 ), $json->partOf ); // phpcs:ignore + $json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore - if ( $page && ( ( \ceil ( $json->totalItems / 20 ) ) > $page ) ) { // phpcs:ignore + if ( $page && ( ( \ceil ( $json->totalItems / $per_page ) ) > $page ) ) { // phpcs:ignore $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore } @@ -90,10 +94,13 @@ class Followers { // phpcs:ignore $json->orderedItems = array_map( - function( $item ) { + function( $item ) use ( $context ) { + if ( 'full' === $context ) { + return $item->to_array(); + } return $item->get_url(); }, - Follower_Collection::get_followers( $user->get__id(), 20, $page ) + $data['followers'] ); $response = new WP_REST_Response( $json, 200 ); @@ -115,11 +122,28 @@ class Followers { 'default' => 1, ); + $params['per_page'] = array( + 'type' => 'integer', + 'default' => 20, + ); + + $params['order'] = array( + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ); + $params['user_id'] = array( 'required' => true, 'type' => 'string', ); + $params['context'] = array( + 'type' => 'string', + 'default' => 'simple', + 'enum' => array( 'simple', 'full' ), + ); + return $params; } } diff --git a/package.json b/package.json index 741ae18..17006ea 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,23 @@ "name": "Matthias Pfefferle", "web": "https://notiz.blog" }, + "scripts": { + "dev": "wp-scripts start", + "build": "wp-scripts build" + }, "license": "MIT", "bugs": { "url": "https://github.com/pfefferle/wordpress-activitypub/issues" }, "homepage": "https://github.com/pfefferle/wordpress-activitypub#readme", "devDependencies": { + "@wordpress/blocks": "^12.11.0", + "@wordpress/components": "^25.0.0", + "@wordpress/data": "^9.4.0", + "@wordpress/dom-ready": "^3.36.0", + "@wordpress/element": "^5.11.0", + "@wordpress/scripts": "^26.5.0", + "classnames": "^2.3.2", "grunt": "^1.1.0", "grunt-checktextdomain": "^1.0.1", "grunt-wp-i18n": "^1.0.3", diff --git a/src/followers/block.json b/src/followers/block.json new file mode 100644 index 0000000..564032f --- /dev/null +++ b/src/followers/block.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "activitypub/followers", + "apiVersion": 3, + "version": "1.0.0", + "title": "Fediverse Followers", + "category": "widgets", + "description": "Display your followers from the Fediverse on your website.", + "textdomain": "activitypub", + "icon": "groups", + "supports": { + "html": false + }, + "attributes": { + "title": { + "type": "string", + "default": "Fediverse Followers" + }, + "selectedUser": { + "type": "string", + "default": "site" + }, + "per_page": { + "type": "number", + "default": 10 + }, + "order": { + "type": "string", + "default": "desc", + "enum": [ "asc", "desc" ] + } + }, + "styles": [ + { "name": "default", "label": "No Lines", "isDefault": true }, + { "name": "with-lines", "label": "Lines" }, + { "name": "compact", "label": "Compact" } + ], + "editorScript": "file:./index.js", + "viewScript": "file:./view.js", + "style": ["file:./style-view.css","wp-block-query-pagination"] +} \ No newline at end of file diff --git a/src/followers/edit.js b/src/followers/edit.js new file mode 100644 index 0000000..19b161f --- /dev/null +++ b/src/followers/edit.js @@ -0,0 +1,68 @@ +import { SelectControl, RangeControl, PanelBody } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; +import { useMemo, useState } from '@wordpress/element'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { Followers } from './followers'; + +export default function Edit( { attributes, setAttributes } ) { + const { order, per_page, selectedUser, className } = attributes; + const blockProps = useBlockProps(); + const [ page, setPage ] = useState( 1 ); + const orderOptions = [ + { label: __( 'New to old', 'activitypub' ), value: 'desc' }, + { label: __( 'Old to new', 'activitypub' ), value: 'asc' }, + ]; + const users = useSelect( ( select ) => select( 'core' ).getUsers( { who: 'authors' } ) ); + const usersOptions = useMemo( () => { + if ( ! users ) { + return []; + } + const withBlogUser =[ { + label: __( 'Whole Site', 'activitypub' ), + value: 'site' + } ]; + return users.reduce( ( acc, user ) => { + acc.push({ + label: user.name, + value: user.id + } ); + return acc; + }, withBlogUser ); + }, [ users ] ); + const setAttributestAndResetPage = ( key ) => { + return ( value ) => { + setPage( 1 ); + setAttributes( { [ key ]: value } ); + }; + } + + return ( +
    + + + + + + + + +
    + ); +} \ No newline at end of file diff --git a/src/followers/followers.js b/src/followers/followers.js new file mode 100644 index 0000000..b756f8f --- /dev/null +++ b/src/followers/followers.js @@ -0,0 +1,105 @@ +import { useState, useEffect } from 'react'; +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import { createInterpolateElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Pagination } from './pagination'; +import { ExternalLink } from '@wordpress/components'; + +const { namespace } = window._activityPubOptions; + +function getPath( userId, per_page, order, page ) { + const path = `/${ namespace }/users/${ userId }/followers`; + const args = { + per_page, + order, + page, + context: 'full' + }; + return addQueryArgs( path, args ); +} + +function usePage() { + const [ page, setPage ] = useState( 1 ); + return [ page, setPage ]; +} + +export function Followers( { + selectedUser, + per_page, + order, + title, + page: passedPage, + setPage: passedSetPage, + className = '' +} ) { + const userId = selectedUser === 'site' ? 0 : selectedUser; + const [ followers, setFollowers ] = useState( [] ); + const [ pages, setPages ] = useState( 0 ); + const [ total, setTotal ] = useState( 0 ); + const [ localPage, setLocalPage ] = usePage(); + const page = passedPage || localPage; + const setPage = passedSetPage || setLocalPage; + const prevLabel = createInterpolateElement( + /* translators: arrow for previous followers link */ + __( ' Less', 'activitypub' ), + { + span: