diff --git a/README.md b/README.md
index fd03554..e83f274 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.4
-**Stable tag:** 1.2.0
+**Stable tag:** 1.3.0
**Requires PHP:** 5.6
**License:** MIT
**License URI:** http://opensource.org/licenses/MIT
@@ -105,6 +105,23 @@ 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).
+### 2.0.0 ###
+
+* Removed: Deprecated Classes
+* Fixed: Normalize attributes that can have mixed value types
+* Added: URL support for WebFinger
+* Added: Make Post-Template filterable
+* Addes: CSS class for ActivityPub comments to allow custom designs
+
+### 1.3.0 ###
+
+* Added: Threaded-Comments support
+* Improved: alt text for avatars in Follow Me/Followers blocks
+* Improved: `Delete`, `Update` and `Follow` Activities
+* Improved: better/more effective handling of `Delete` Activities
+* Improved: allow `
` and `
` for Comments
+* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
+
### 1.2.0 ###
* Add: Search and order followerer lists
diff --git a/activitypub.php b/activitypub.php
index cacdce2..5179fda 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: 1.2.0
+ * Version: 1.3.0
* Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/
* License: MIT
@@ -69,7 +69,7 @@ function plugin_init() {
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
- \add_action( 'init', array( __NAMESPACE__ . '\Collection\Followers', 'init' ) );
+ \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
@@ -174,7 +174,7 @@ function plugin_settings_link( $actions ) {
*/
add_action(
'bp_include',
- function() {
+ function () {
require_once __DIR__ . '/integration/class-buddypress.php';
Integration\Buddypress::init();
},
diff --git a/build/follow-me/index.asset.php b/build/follow-me/index.asset.php
index 760f433..c6138c7 100644
--- a/build/follow-me/index.asset.php
+++ b/build/follow-me/index.asset.php
@@ -1 +1 @@
- array('wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '6aeec6336fd28aa836a7');
+ array( 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '6aeec6336fd28aa836a7' );
diff --git a/build/follow-me/view.asset.php b/build/follow-me/view.asset.php
index 76495ef..1c33822 100644
--- a/build/follow-me/view.asset.php
+++ b/build/follow-me/view.asset.php
@@ -1 +1 @@
- array('wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '5b48281e37700a970a66');
+ array( 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '5b48281e37700a970a66' );
diff --git a/build/followers/index.asset.php b/build/followers/index.asset.php
index 324ac5e..fec0ba9 100644
--- a/build/followers/index.asset.php
+++ b/build/followers/index.asset.php
@@ -1 +1 @@
- array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '59d9702e06860a6d13e4');
+ array( 'react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url' ), 'version' => '59d9702e06860a6d13e4' );
diff --git a/build/followers/view.asset.php b/build/followers/view.asset.php
index ba6c322..305d9f5 100644
--- a/build/followers/view.asset.php
+++ b/build/followers/view.asset.php
@@ -1 +1 @@
- array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '04e51e7562fe28b0b2c3');
+ array( 'react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url' ), 'version' => '04e51e7562fe28b0b2c3' );
diff --git a/composer.json b/composer.json
index 054226f..604ab85 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,8 @@
"yoast/phpunit-polyfills": "^2.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
"sirbrillig/phpcs-variable-analysis": "^2.11",
- "phpcsstandards/phpcsextra": "^1.1.0"
+ "phpcsstandards/phpcsextra": "^1.1.0",
+ "dms/phpunit-arraysubset-asserts": "^0.5.0"
},
"config": {
"allow-plugins": true
diff --git a/includes/activity/class-activity.php b/includes/activity/class-activity.php
index 6c59866..96ee095 100644
--- a/includes/activity/class-activity.php
+++ b/includes/activity/class-activity.php
@@ -194,6 +194,12 @@ class Activity extends Base_Object {
* @return void
*/
public function set_object( $object ) {
+ // convert array to object
+ if ( is_array( $object ) ) {
+ $object = Base_Object::init_from_array( $object );
+ }
+
+ // set object
$this->set( 'object', $object );
if ( ! is_object( $object ) ) {
diff --git a/includes/activity/class-base-object.php b/includes/activity/class-base-object.php
index 9b9f044..3e25105 100644
--- a/includes/activity/class-base-object.php
+++ b/includes/activity/class-base-object.php
@@ -644,7 +644,7 @@ class Base_Object {
foreach ( $array as $key => $value ) {
$key = camel_to_snake_case( $key );
- $object->set( $key, $value );
+ call_user_func( array( $object, 'set_' . $key ), $value );
}
return $object;
@@ -670,7 +670,7 @@ class Base_Object {
foreach ( $array as $key => $value ) {
if ( $value ) {
$key = camel_to_snake_case( $key );
- $this->set( $key, $value );
+ call_user_func( array( $this, 'set_' . $key ), $value );
}
}
}
diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php
index 0b768a7..6a07b2d 100644
--- a/includes/class-activitypub.php
+++ b/includes/class-activitypub.php
@@ -1,8 +1,12 @@
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_post_meta(
+ Followers::POST_TYPE,
+ 'activitypub_inbox',
+ array(
+ 'type' => 'string',
+ 'single' => true,
+ 'sanitize_callback' => 'sanitize_url',
+ )
+ );
+
+ register_post_meta(
+ Followers::POST_TYPE,
+ 'activitypub_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 );
+ },
+ )
+ );
+
+ register_post_meta(
+ Followers::POST_TYPE,
+ 'activitypub_user_id',
+ array(
+ 'type' => 'string',
+ 'single' => false,
+ 'sanitize_callback' => function ( $value ) {
+ return esc_sql( $value );
+ },
+ )
+ );
+
+ register_post_meta(
+ Followers::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' );
+ }
+
+ /**
+ * Filters the CSS classes to add an ActivityPub class.
+ *
+ * @param string[] $classes An array of comment classes.
+ * @param string[] $css_class An array of additional classes added to the list.
+ * @param string $comment_id The comment ID as a numeric string.
+ *
+ * @return string[] An array of classes.
+ */
+ public static function comment_class( $classes, $css_class, $comment_id ) {
+ // check if ActivityPub comment
+ if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
+ $classes[] = 'activitypub-comment';
+ }
+
+ return $classes;
+ }
}
diff --git a/includes/class-admin.php b/includes/class-admin.php
index 4d8655a..1889e41 100644
--- a/includes/class-admin.php
+++ b/includes/class-admin.php
@@ -215,7 +215,7 @@ class Admin {
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
'show_in_rest' => true,
'default' => Blog_User::get_default_username(),
- 'sanitize_callback' => function( $value ) {
+ 'sanitize_callback' => function ( $value ) {
// hack to allow dots in the username
$parts = explode( '.', $value );
$sanitized = array();
diff --git a/includes/class-blocks.php b/includes/class-blocks.php
index 02f9659..c127795 100644
--- a/includes/class-blocks.php
+++ b/includes/class-blocks.php
@@ -3,7 +3,9 @@ namespace Activitypub;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Users as User_Collection;
-use Activitypub\is_user_type_disabled;
+
+use function Activitypub\object_to_uri;
+use function Activitypub\is_user_type_disabled;
class Blocks {
public static function init() {
@@ -94,7 +96,7 @@ class Blocks {
$attrs['followerData']['total'] = $follower_data['total'];
$attrs['followerData']['followers'] = array_map(
- function( $follower ) {
+ function ( $follower ) {
return self::filter_array_by_keys(
$follower->to_array(),
array( 'icon', 'name', 'preferredUsername', 'url' )
@@ -140,7 +142,7 @@ class Blocks {
return sprintf(
$template,
- esc_url( $data['url'] ),
+ esc_url( object_to_uri( $data['url'] ) ),
esc_attr( $data['name'] ),
esc_attr( $data['icon']['url'] ),
esc_html( $data['name'] ),
diff --git a/includes/class-handler.php b/includes/class-handler.php
new file mode 100644
index 0000000..fcabd63
--- /dev/null
+++ b/includes/class-handler.php
@@ -0,0 +1,33 @@
+ 'ID',
+ 'capability__in' => array( 'publish_posts' ),
+ )
+ );
+
+ foreach ( $user_ids as $user_id ) {
+ wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
+ }
+ }
}
diff --git a/includes/class-signature.php b/includes/class-signature.php
index d021cf0..e59a1f9 100644
--- a/includes/class-signature.php
+++ b/includes/class-signature.php
@@ -292,7 +292,8 @@ class Signature {
if ( is_array( $headers['digest'] ) ) {
$headers['digest'] = $headers['digest'][0];
}
- $digest = explode( '=', $headers['digest'], 2 );
+ $hashalg = 'sha256';
+ $digest = explode( '=', $headers['digest'], 2 );
if ( 'SHA-256' === $digest[0] ) {
$hashalg = 'sha256';
}
diff --git a/includes/collection/class-followers.php b/includes/collection/class-followers.php
index c2ad01f..b51224f 100644
--- a/includes/collection/class-followers.php
+++ b/includes/collection/class-followers.php
@@ -2,14 +2,10 @@
namespace Activitypub\Collection;
use WP_Error;
-use Exception;
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;
@@ -24,136 +20,6 @@ class Followers {
const POST_TYPE = 'ap_follower';
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
- /**
- * Register WordPress hooks/actions and register Taxonomy
- *
- * @return void
- */
- public static function init() {
- // 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 );
-
- \add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
- }
-
- /**
- * Register the "Followers" Taxonomy
- *
- * @return void
- */
- 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_post_meta(
- self::POST_TYPE,
- 'activitypub_inbox',
- array(
- 'type' => 'string',
- 'single' => true,
- 'sanitize_callback' => array( self::class, 'sanitize_url' ),
- )
- );
-
- register_post_meta(
- self::POST_TYPE,
- 'activitypub_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 );
- },
- )
- );
-
- register_post_meta(
- self::POST_TYPE,
- 'activitypub_user_id',
- array(
- 'type' => 'string',
- 'single' => false,
- 'sanitize_callback' => function( $value ) {
- return esc_sql( $value );
- },
- )
- );
-
- 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' );
- }
-
- public static function sanitize_url( $value ) {
- if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
- return null;
- }
-
- return esc_url_raw( $value );
- }
-
- /**
- * Handle the "Follow" Request
- *
- * @param array $object The JSON "Follow" Activity
- * @param int $user_id The ID of the ID of the WordPress User
- *
- * @return void
- */
- public static function handle_follow_request( $object, $user_id ) {
- // save follower
- $follower = self::add_follower( $user_id, $object['actor'] );
-
- do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower );
- }
-
- /**
- * Handle "Unfollow" requests
- *
- * @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 (
- isset( $object['object'] ) &&
- isset( $object['object']['type'] ) &&
- 'Follow' === $object['object']['type']
- ) {
- self::remove_follower( $user_id, $object['actor'] );
- }
- }
-
/**
* Add new Follower
*
@@ -214,16 +80,17 @@ class Followers {
}
/**
- * Get 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
+ * @return \Activitypub\Model\Follower|null The Follower object or null
*/
public static function get_follower( $user_id, $actor ) {
global $wpdb;
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$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 = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
@@ -244,51 +111,29 @@ class Followers {
}
/**
- * Send Accept response
+ * Get a Follower by Actor indepenent from the User.
*
- * @param string $actor The Actor URL
- * @param array $object The Activity object
- * @param int $user_id The ID of the WordPress User
- * @param Activitypub\Model\Follower $follower The Follower object
+ * @param string $actor The Actor URL.
*
- * @return void
+ * @return \Activitypub\Model\Follower|null The Follower object or null
*/
- public static function send_follow_response( $actor, $object, $user_id, $follower ) {
- if ( is_wp_error( $follower ) ) {
- // it is not even possible to send a "Reject" because
- // we can not get the Remote-Inbox
- return;
- }
+ public static function get_follower_by_actor( $actor ) {
+ global $wpdb;
- // only send minimal data
- $object = array_intersect_key(
- $object,
- array_flip(
- array(
- 'id',
- 'type',
- 'actor',
- 'object',
- )
+ // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $post_id = $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT ID FROM $wpdb->posts WHERE guid=%s",
+ esc_sql( $actor )
)
);
- $user = Users::get_by_id( $user_id );
+ if ( $post_id ) {
+ $post = get_post( $post_id );
+ return Follower::init_from_cpt( $post );
+ }
- // get inbox
- $inbox = $follower->get_shared_inbox();
-
- // send "Accept" activity
- $activity = new Activity();
- $activity->set_type( 'Accept' );
- $activity->set_object( $object );
- $activity->set_actor( $user->get_id() );
- $activity->set_to( $actor );
- $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
-
- $activity = $activity->to_json();
-
- Http::post( $inbox, $activity, $user_id );
+ return null;
}
/**
@@ -337,7 +182,7 @@ class Followers {
$query = new WP_Query( $args );
$total = $query->found_posts;
$followers = array_map(
- function( $post ) {
+ function ( $post ) {
return Follower::init_from_cpt( $post );
},
$query->get_posts()
@@ -354,6 +199,7 @@ class Followers {
*/
public static function get_all_followers() {
$args = array(
+ 'nopaging' => true,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
@@ -422,6 +268,7 @@ class Followers {
// get all Followers of a ID of the WordPress User
$posts = new WP_Query(
array(
+ 'nopaging' => true,
'post_type' => self::POST_TYPE,
'fields' => 'ids',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
diff --git a/includes/collection/class-interactions.php b/includes/collection/class-interactions.php
new file mode 100644
index 0000000..87e4334
--- /dev/null
+++ b/includes/collection/class-interactions.php
@@ -0,0 +1,235 @@
+comment_post_ID;
+ }
+
+ // not a reply to a post or comment
+ if ( ! $comment_post_id ) {
+ return false;
+ }
+
+ $meta = get_remote_metadata_by_actor( $activity['actor'] );
+
+ if ( ! $meta || \is_wp_error( $meta ) ) {
+ return false;
+ }
+
+ $commentdata = array(
+ 'comment_post_ID' => $comment_post_id,
+ 'comment_author' => \esc_attr( $meta['name'] ),
+ 'comment_author_url' => \esc_url_raw( $meta['url'] ),
+ 'comment_content' => \addslashes( $activity['object']['content'] ),
+ 'comment_type' => 'comment',
+ 'comment_author_email' => '',
+ 'comment_parent' => $parent_comment ? $parent_comment->comment_ID : 0,
+ 'comment_meta' => array(
+ 'source_id' => \esc_url_raw( $activity['object']['id'] ),
+ 'source_url' => \esc_url_raw( $activity['object']['url'] ),
+ 'protocol' => 'activitypub',
+ ),
+ );
+
+ if ( isset( $meta['icon']['url'] ) ) {
+ $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
+ }
+
+ // disable flood control
+ \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
+ // do not require email for AP entries
+ \add_filter( 'pre_option_require_name_email', '__return_false' );
+ // No nonce possible for this submission route
+ \add_filter(
+ 'akismet_comment_nonce',
+ function () {
+ return 'inactive';
+ }
+ );
+ \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
+
+ $comment = \wp_new_comment( $commentdata, true );
+
+ \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
+ \remove_filter( 'pre_option_require_name_email', '__return_false' );
+ // re-add flood control
+ \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
+
+ return $comment;
+ }
+
+ /**
+ * Update a comment
+ *
+ * @param array $activity The activity-object
+ *
+ * @return array|false The commentdata or false on failure
+ */
+ public static function update_comment( $activity ) {
+ $meta = get_remote_metadata_by_actor( $activity['actor'] );
+
+ //Determine comment_ID
+ $object_comment_id = url_to_commentid( \esc_url_raw( $activity['object']['id'] ) );
+
+ if ( ! $object_comment_id ) {
+ return false;
+ }
+
+ //found a local comment id
+ $commentdata = \get_comment( $object_comment_id, ARRAY_A );
+ $commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
+ $commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
+ if ( isset( $meta['icon']['url'] ) ) {
+ $commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
+ }
+
+ // disable flood control
+ \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
+ // do not require email for AP entries
+ \add_filter( 'pre_option_require_name_email', '__return_false' );
+ // No nonce possible for this submission route
+ \add_filter(
+ 'akismet_comment_nonce',
+ function () {
+ return 'inactive';
+ }
+ );
+ \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
+
+ $comment = \wp_update_comment( $commentdata, true );
+
+ \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
+ \remove_filter( 'pre_option_require_name_email', '__return_false' );
+ // re-add flood control
+ \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
+
+ return $comment;
+ }
+
+ /**
+ * Get interaction(s) for a given URL/ID.
+ *
+ * @param strin $url The URL/ID to get interactions for.
+ *
+ * @return array The interactions as WP_Comment objects.
+ */
+ public static function get_interaction_by_id( $url ) {
+ $args = array(
+ 'nopaging' => true,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ 'relation' => 'AND',
+ array(
+ 'key' => 'protocol',
+ 'value' => 'activitypub',
+ ),
+ array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'source_url',
+ 'value' => $url,
+ ),
+ array(
+ 'key' => 'source_id',
+ 'value' => $url,
+ ),
+ ),
+ ),
+ );
+
+ $query = new WP_Comment_Query( $args );
+ return $query->comments;
+ }
+
+ /**
+ * Get interaction(s) for a given actor.
+ *
+ * @param string $actor The Actor-URL.
+ *
+ * @return array The interactions as WP_Comment objects.
+ */
+ public static function get_interactions_by_actor( $actor ) {
+ $meta = get_remote_metadata_by_actor( $actor );
+
+ // get URL, because $actor seems to be the ID
+ if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
+ $actor = $meta['url'];
+ }
+
+ $args = array(
+ 'nopaging' => true,
+ 'author_url' => $actor,
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ array(
+ 'key' => 'protocol',
+ 'value' => 'activitypub',
+ 'compare' => '=',
+ ),
+ ),
+ );
+ $comment_query = new WP_Comment_Query( $args );
+ return $comment_query->comments;
+ }
+
+ /**
+ * Adds line breaks to the list of allowed comment tags.
+ *
+ * @param array $allowed_tags Allowed HTML tags.
+ * @param string $context Context.
+ *
+ * @return array Filtered tag list.
+ */
+ public static function allowed_comment_html( $allowed_tags, $context = '' ) {
+ if ( 'pre_comment_content' !== $context ) {
+ // Do nothing.
+ return $allowed_tags;
+ }
+
+ // Add `p` and `br` to the list of allowed tags.
+ if ( ! array_key_exists( 'br', $allowed_tags ) ) {
+ $allowed_tags['br'] = array();
+ }
+
+ if ( ! array_key_exists( 'p', $allowed_tags ) ) {
+ $allowed_tags['p'] = array();
+ }
+
+ return $allowed_tags;
+ }
+}
diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php
index f6e35a6..ad94297 100644
--- a/includes/collection/class-users.php
+++ b/includes/collection/class-users.php
@@ -7,6 +7,7 @@ use Activitypub\Model\User;
use Activitypub\Model\Blog_User;
use Activitypub\Model\Application_User;
+use function Activitypub\url_to_authorid;
use function Activitypub\is_user_disabled;
class Users {
@@ -103,6 +104,8 @@ class Users {
return self::get_by_id( $user->results[0] );
}
+ $username = str_replace( array( '*', '%' ), '', $username );
+
// check for login or nicename.
$user = new WP_User_Query(
array(
@@ -133,29 +136,79 @@ class Users {
* @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 )
- );
+ $scheme = 'acct';
+ $match = array();
+ // try to extract the scheme and the host
+ if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
+ // extract the scheme
+ $scheme = esc_attr( $match[1] );
}
- $resource = \str_replace( 'acct:', '', $resource );
+ switch ( $scheme ) {
+ // check for http(s) URIs
+ case 'http':
+ case 'https':
+ $url_parts = wp_parse_url( $resource );
- $resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
- $resource_host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
- $blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
+ // check for http(s)://blog.example.com/@username
+ if (
+ isset( $url_parts['path'] ) &&
+ str_starts_with( $url_parts['path'], '/@' )
+ ) {
+ $identifier = str_replace( '/@', '', $url_parts['path'] );
+ $identifier = untrailingslashit( $identifier );
- 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( $identifier );
+ }
+
+ // check for http(s)://blog.example.com/author/username
+ $user_id = url_to_authorid( $resource );
+
+ if ( $user_id ) {
+ return self::get_by_id( $user_id );
+ }
+
+ // check for http(s)://blog.example.com/
+ if (
+ self::normalize_url( site_url() ) === self::normalize_url( $resource ) ||
+ self::normalize_url( home_url() ) === self::normalize_url( $resource )
+ ) {
+ return self::get_by_id( self::BLOG_USER_ID );
+ }
+
+ return new WP_Error(
+ 'activitypub_no_user_found',
+ \__( 'User not found', 'activitypub' ),
+ array( 'status' => 404 )
+ );
+ // check for acct URIs
+ case 'acct':
+ $resource = \str_replace( 'acct:', '', $resource );
+ $identifier = \substr( $resource, 0, \strrpos( $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 !== $host ) {
+ return new WP_Error(
+ 'activitypub_wrong_host',
+ \__( 'Resource host does not match blog host', 'activitypub' ),
+ array( 'status' => 404 )
+ );
+ }
+
+ // prepare wildcards https://github.com/mastodon/mastodon/issues/22213
+ if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
+ return self::get_by_id( self::BLOG_USER_ID );
+ }
+
+ return self::get_by_username( $identifier );
+ default:
+ return new WP_Error(
+ 'activitypub_wrong_scheme',
+ \__( 'Wrong scheme', 'activitypub' ),
+ array( 'status' => 404 )
+ );
}
-
- return self::get_by_username( $resource_identifier );
}
/**
@@ -168,7 +221,12 @@ class Users {
public static function get_by_various( $id ) {
if ( is_numeric( $id ) ) {
return self::get_by_id( $id );
- } elseif ( filter_var( $id, FILTER_VALIDATE_URL ) ) {
+ } elseif (
+ // is URL
+ filter_var( $id, FILTER_VALIDATE_URL ) ||
+ // is acct
+ str_starts_with( $id, 'acct:' )
+ ) {
return self::get_by_resource( $id );
} else {
return self::get_by_username( $id );
@@ -176,7 +234,7 @@ class Users {
}
/**
- * Normalize the host.
+ * Normalize a host.
*
* @param string $host The host.
*
@@ -186,6 +244,22 @@ class Users {
return \str_replace( 'www.', '', $host );
}
+ /**
+ * Normalize a URL.
+ *
+ * @param string $url The URL.
+ *
+ * @return string The normalized URL.
+ */
+ public static function normalize_url( $url ) {
+ $url = \untrailingslashit( $url );
+ $url = \str_replace( 'https://', '', $url );
+ $url = \str_replace( 'http://', '', $url );
+ $url = \str_replace( 'www.', '', $url );
+
+ return $url;
+ }
+
/**
* Get the User collection.
*
diff --git a/includes/compat.php b/includes/compat.php
index 3dd405c..d1047df 100644
--- a/includes/compat.php
+++ b/includes/compat.php
@@ -44,6 +44,35 @@ if ( ! function_exists( 'is_countable' ) ) {
* @return bool True if `$value` is countable, otherwise false.
*/
function is_countable( $value ) {
- return is_array( $value ) || $value instanceof \Countable;
+ return is_array( $value ) || $value instanceof \Countable;
+ }
+}
+
+/**
+ * Polyfill for `array_is_list()` function added in PHP 7.3.
+ *
+ * @param array $array The array to check.
+ *
+ * @return bool True if `$array` is a list, otherwise false.
+ */
+if ( ! function_exists( 'array_is_list' ) ) {
+ function array_is_list( $array ) {
+ if ( ! is_array( $array ) ) {
+ return false;
+ }
+
+ if ( array_values( $array ) === $array ) {
+ return true;
+ }
+
+ $next_key = -1;
+
+ foreach ( $array as $k => $v ) {
+ if ( ++$next_key !== $k ) {
+ return false;
+ }
+ }
+
+ return true;
}
}
diff --git a/includes/functions.php b/includes/functions.php
index f9d602f..1140724 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -2,6 +2,7 @@
namespace Activitypub;
use WP_Error;
+use WP_Comment_Query;
use Activitypub\Http;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers;
@@ -486,6 +487,81 @@ function is_blog_public() {
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
}
+/**
+ * Sanitize a URL
+ *
+ * @param string $value The URL to sanitize
+ *
+ * @return string|null The sanitized URL or null if invalid
+ */
+function sanitize_url( $value ) {
+ if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
+ return null;
+ }
+
+ return esc_url_raw( $value );
+}
+
+/**
+ * Extract recipient URLs from Activity object
+ *
+ * @param array $data
+ *
+ * @return array The list of user URLs
+ */
+function extract_recipients_from_activity( $data ) {
+ $recipient_items = array();
+
+ foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
+ if ( array_key_exists( $i, $data ) ) {
+ if ( is_array( $data[ $i ] ) ) {
+ $recipient = $data[ $i ];
+ } else {
+ $recipient = array( $data[ $i ] );
+ }
+ $recipient_items = array_merge( $recipient_items, $recipient );
+ }
+
+ if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
+ if ( is_array( $data['object'][ $i ] ) ) {
+ $recipient = $data['object'][ $i ];
+ } else {
+ $recipient = array( $data['object'][ $i ] );
+ }
+ $recipient_items = array_merge( $recipient_items, $recipient );
+ }
+ }
+
+ $recipients = array();
+
+ // flatten array
+ foreach ( $recipient_items as $recipient ) {
+ if ( is_array( $recipient ) ) {
+ // check if recipient is an object
+ if ( array_key_exists( 'id', $recipient ) ) {
+ $recipients[] = $recipient['id'];
+ }
+ } else {
+ $recipients[] = $recipient;
+ }
+ }
+
+ return array_unique( $recipients );
+}
+
+/**
+ * Check if passed Activity is Public
+ *
+ * @param array $data The Activity object as array
+ *
+ * @return boolean True if public, false if not
+ */
+function is_activity_public( $data ) {
+ $recipients = extract_recipients_from_activity( $data );
+
+ return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
+}
+
/**
* Get active users based on a given duration
*
@@ -557,3 +633,105 @@ function get_total_users() {
return $users + 1;
}
+
+/**
+ * Examine a comment ID and look up an existing comment it represents.
+ *
+ * @param string $id ActivityPub object ID (usually a URL) to check.
+ *
+ * @return int|boolean Comment ID, or false on failure.
+ */
+function object_id_to_comment( $id ) {
+ $comment_query = new WP_Comment_Query(
+ array(
+ 'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+ )
+ );
+
+ if ( ! $comment_query->comments ) {
+ return false;
+ }
+
+ if ( count( $comment_query->comments ) > 1 ) {
+ return false;
+ }
+
+ return $comment_query->comments[0];
+}
+
+/**
+ * Verify if URL is a local comment,
+ * Or if it is a previously received remote comment
+ * (For threading comments locally)
+ *
+ * @param string $url The URL to check.
+ *
+ * @return int comment_ID or null if not found
+ */
+function url_to_commentid( $url ) {
+ if ( ! $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
+ return null;
+ }
+
+ $args = array(
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'meta_query' => array(
+ 'relation' => 'OR',
+ array(
+ 'key' => 'source_url',
+ 'value' => $url,
+ ),
+ array(
+ 'key' => 'source_id',
+ 'value' => $url,
+ ),
+ ),
+ );
+
+ $query = new \WP_Comment_Query();
+ $comments = $query->query( $args );
+
+ if ( $comments && is_array( $comments ) ) {
+ return $comments[0]->comment_ID;
+ }
+
+ return null;
+}
+
+/**
+ * Get the URI of an ActivityPub object
+ *
+ * @param array $object The ActivityPub object
+ *
+ * @return string The URI of the ActivityPub object
+ */
+function object_to_uri( $object ) {
+ // check if it is already simple
+ if ( ! $object || is_string( $object ) ) {
+ return $object;
+ }
+
+ // check if it is a list, then take first item
+ // this plugin does not support collections
+ if ( array_is_list( $object ) ) {
+ $object = $object[0];
+ }
+
+ // check if it is simplified now
+ if ( is_string( $object ) ) {
+ return $object;
+ }
+
+ // return part of Object that makes most sense
+ switch ( $object['type'] ) {
+ case 'Link':
+ $object = $object['href'];
+ break;
+ default:
+ $object = $object['id'];
+ break;
+ }
+
+ return $object;
+}
diff --git a/includes/handler/class-create.php b/includes/handler/class-create.php
new file mode 100644
index 0000000..2e5d76a
--- /dev/null
+++ b/includes/handler/class-create.php
@@ -0,0 +1,61 @@
+delete();
+ }
+ }
+
+ /**
+ * Delete Reactions if Actor-URL is a Tombstone.
+ *
+ * @param array $activity The delete activity.
+ */
+ public static function maybe_delete_interactions( $activity ) {
+ // verify if Actor is deleted.
+ if ( Http::is_tombstone( $activity['actor'] ) ) {
+ \wp_schedule_single_event(
+ \time(),
+ 'activitypub_delete_actor_interactions',
+ array( $activity['actor'] )
+ );
+ }
+ }
+
+ /**
+ * Delete comments from an Actor.
+ *
+ * @param array $comments The comments to delete.
+ */
+ public static function delete_interactions( $actor ) {
+ $comments = Interactions::get_interactions_by_actor( $actor );
+
+ if ( is_array( $comments ) ) {
+ foreach ( $comments as $comment ) {
+ wp_delete_comment( $comment->comment_ID );
+ }
+ }
+ }
+
+ /**
+ * Delete a Reaction if URL is a Tombstone.
+ *
+ * @param array $activity The delete activity.
+ *
+ * @return void
+ */
+ public static function maybe_delete_interaction( $activity ) {
+ if ( is_array( $activity['object'] ) ) {
+ $id = $activity['object']['id'];
+ } else {
+ $id = $activity['object'];
+ }
+
+ $comments = Interactions::get_interaction_by_id( $id );
+
+ if ( $comments && Http::is_tombstone( $id ) ) {
+ foreach ( $comments as $comment ) {
+ wp_delete_comment( $comment->comment_ID, true );
+ }
+ }
+ }
+
+ /**
+ * Defer signature verification for `Delete` requests.
+ *
+ * @param bool $defer Whether to defer signature verification.
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return bool Whether to defer signature verification.
+ */
+ public static function defer_signature_verification( $defer, $request ) {
+ $json = $request->get_json_params();
+
+ if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/includes/handler/class-follow.php b/includes/handler/class-follow.php
new file mode 100644
index 0000000..6855dbd
--- /dev/null
+++ b/includes/handler/class-follow.php
@@ -0,0 +1,81 @@
+get_shared_inbox();
+
+ // send "Accept" activity
+ $activity = new Activity();
+ $activity->set_type( 'Accept' );
+ $activity->set_object( $object );
+ $activity->set_actor( $user->get_id() );
+ $activity->set_to( $actor );
+ $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
+
+ $activity = $activity->to_json();
+
+ Http::post( $inbox, $activity, $user_id );
+ }
+}
diff --git a/includes/handler/class-undo.php b/includes/handler/class-undo.php
new file mode 100644
index 0000000..13c06f3
--- /dev/null
+++ b/includes/handler/class-undo.php
@@ -0,0 +1,31 @@
+post = $post;
- $transformer = new Transformer_Post();
- $this->object = $transformer->transform( $post );
- }
-
- /**
- * Returns the User ID.
- *
- * @return int the User ID.
- */
- public function get_user_id() {
- return apply_filters( 'activitypub_post_user_id', $this->post->post_author, $this->post );
- }
-
- /**
- * Converts this Object into an Array.
- *
- * @return array the array representation of a Post.
- */
- public function to_array() {
- return \apply_filters( 'activitypub_post', $this->object->to_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
- *
- * @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() {
- return $this->object->get_url();
- }
-
- /**
- * Returns the ID of an Activity Object
- *
- * @return string
- */
- public function get_id() {
- return $this->object->get_id();
- }
-
- /**
- * Returns a list of Image Attachments
- *
- * @return array
- */
- public function get_attachments() {
- return $this->object->get_attachment();
- }
-
- /**
- * Returns a list of Tags, used in the Post
- *
- * @return array
- */
- public function get_tags() {
- return $this->object->get_tag();
- }
-
- /**
- * Returns the as2 object-type for a given post
- *
- * @return string the object-type
- */
- public function get_object_type() {
- return $this->object->get_type();
- }
-
- /**
- * Returns the content for the ActivityPub Item.
- *
- * @return string the content
- */
- public function get_content() {
- return $this->object->get_content();
- }
-}
diff --git a/includes/peer/class-followers.php b/includes/peer/class-followers.php
deleted file mode 100644
index e0e6ddb..0000000
--- a/includes/peer/class-followers.php
+++ /dev/null
@@ -1,34 +0,0 @@
-orderedItems = array_map(
- function( $item ) use ( $context ) {
+ function ( $item ) use ( $context ) {
if ( 'full' === $context ) {
return $item->to_array();
}
diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php
index 15fb4b2..938ca90 100644
--- a/includes/rest/class-inbox.php
+++ b/includes/rest/class-inbox.php
@@ -8,9 +8,11 @@ use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\get_context;
+use function Activitypub\object_to_uri;
use function Activitypub\url_to_authorid;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_remote_metadata_by_actor;
+use function Activitypub\extract_recipients_from_activity;
/**
* ActivityPub Inbox REST-Class
@@ -25,8 +27,6 @@ class Inbox {
*/
public static function init() {
self::register_routes();
-
- \add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
}
/**
@@ -130,12 +130,13 @@ class Inbox {
return $user;
}
- $data = $request->get_json_params();
- $type = $request->get_param( 'type' );
- $type = \strtolower( $type );
+ $data = $request->get_json_params();
+ $activity = Activity::init_from_array( $data );
+ $type = $request->get_param( 'type' );
+ $type = \strtolower( $type );
- \do_action( 'activitypub_inbox', $data, $user->get__id(), $type );
- \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() );
+ \do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity );
+ \do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity );
$rest_response = new WP_REST_Response( array(), 202 );
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
@@ -151,9 +152,10 @@ class Inbox {
* @return WP_REST_Response
*/
public static function shared_inbox_post( $request ) {
- $data = $request->get_json_params();
- $type = $request->get_param( 'type' );
- $users = self::extract_recipients( $data );
+ $data = $request->get_json_params();
+ $activity = Activity::init_from_array( $data );
+ $type = $request->get_param( 'type' );
+ $users = self::get_recipients( $data );
if ( ! $users ) {
return new WP_Error(
@@ -181,8 +183,8 @@ class Inbox {
$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->ID, $type, $activity );
+ \do_action( "activitypub_inbox_{$type}", $data, $user->ID, $activity );
}
$rest_response = new WP_REST_Response( array(), 202 );
@@ -235,15 +237,8 @@ class Inbox {
$params['actor'] = array(
'required' => true,
- 'sanitize_callback' => function( $param, $request, $key ) {
- if ( \is_array( $param ) ) {
- if ( isset( $param['id'] ) ) {
- $param = $param['id'];
- } else {
- $param = $param['url'];
- }
- }
- return \esc_url_raw( $param );
+ 'sanitize_callback' => function ( $param, $request, $key ) {
+ return object_to_uri( $param );
},
);
@@ -251,7 +246,7 @@ class Inbox {
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
- //'sanitize_callback' => function( $param, $request, $key ) {
+ //'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
@@ -284,11 +279,8 @@ class Inbox {
$params['actor'] = array(
'required' => true,
//'type' => array( 'object', 'string' ),
- 'sanitize_callback' => function( $param, $request, $key ) {
- if ( ! \is_string( $param ) ) {
- $param = $param['id'];
- }
- return \esc_url_raw( $param );
+ 'sanitize_callback' => function ( $param, $request, $key ) {
+ return object_to_uri( $param );
},
);
@@ -296,7 +288,7 @@ class Inbox {
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
- //'sanitize_callback' => function( $param, $request, $key ) {
+ //'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
@@ -308,7 +300,7 @@ class Inbox {
$params['to'] = array(
'required' => false,
- 'sanitize_callback' => function( $param, $request, $key ) {
+ 'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@@ -318,7 +310,7 @@ class Inbox {
);
$params['cc'] = array(
- 'sanitize_callback' => function( $param, $request, $key ) {
+ 'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@@ -328,7 +320,7 @@ class Inbox {
);
$params['bcc'] = array(
- 'sanitize_callback' => function( $param, $request, $key ) {
+ 'sanitize_callback' => function ( $param, $request, $key ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@@ -340,121 +332,6 @@ class Inbox {
return $params;
}
- /**
- * Handles "Create" requests
- *
- * @param array $object The activity-object
- * @param int $user_id The id of the local blog-user
- */
- public static function handle_create( $object, $user_id ) {
- $meta = get_remote_metadata_by_actor( $object['actor'] );
-
- if ( ! isset( $object['object']['inReplyTo'] ) ) {
- return;
- }
-
- // check if Activity is public or not
- if ( ! self::is_activity_public( $object ) ) {
- // @todo maybe send email
- return;
- }
-
- $comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
-
- // save only replys and reactions
- if ( ! $comment_post_id ) {
- return false;
- }
-
- $commentdata = array(
- 'comment_post_ID' => $comment_post_id,
- 'comment_author' => \esc_attr( $meta['name'] ),
- 'comment_author_url' => \esc_url_raw( $object['actor'] ),
- 'comment_content' => addslashes( \wp_kses( $object['object']['content'], 'pre_comment_content' ) ),
- 'comment_type' => 'comment',
- 'comment_author_email' => '',
- 'comment_parent' => 0,
- 'comment_meta' => array(
- 'source_url' => \esc_url_raw( $object['object']['url'] ),
- 'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
- 'protocol' => 'activitypub',
- ),
- );
-
- // disable flood control
- \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
-
- // do not require email for AP entries
- \add_filter( 'pre_option_require_name_email', '__return_false' );
-
- // No nonce possible for this submission route
- \add_filter(
- 'akismet_comment_nonce',
- function() {
- return 'inactive';
- }
- );
-
- \add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
-
- $state = \wp_new_comment( $commentdata, true );
-
- \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
- \remove_filter( 'pre_option_require_name_email', '__return_false' );
-
- // re-add flood control
- \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
-
- do_action( 'activitypub_handled_create', $object, $user_id, $state, $commentdata );
- }
-
- /**
- * Extract recipient URLs from Activity object
- *
- * @param array $data
- *
- * @return array The list of user URLs
- */
- public static function extract_recipients( $data ) {
- $recipient_items = array();
-
- foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
- if ( array_key_exists( $i, $data ) ) {
- if ( is_array( $data[ $i ] ) ) {
- $recipient = $data[ $i ];
- } else {
- $recipient = array( $data[ $i ] );
- }
- $recipient_items = array_merge( $recipient_items, $recipient );
- }
-
- if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
- if ( is_array( $data['object'][ $i ] ) ) {
- $recipient = $data['object'][ $i ];
- } else {
- $recipient = array( $data['object'][ $i ] );
- }
- $recipient_items = array_merge( $recipient_items, $recipient );
- }
- }
-
- $recipients = array();
-
- // flatten array
- foreach ( $recipient_items as $recipient ) {
- if ( is_array( $recipient ) ) {
- // check if recipient is an object
- if ( array_key_exists( 'id', $recipient ) ) {
- $recipients[] = $recipient['id'];
- }
- } else {
- $recipients[] = $recipient;
- }
- }
-
- return array_unique( $recipients );
- }
-
/**
* Get local user recipients
*
@@ -463,7 +340,7 @@ class Inbox {
* @return array The list of local users
*/
public static function get_recipients( $data ) {
- $recipients = self::extract_recipients( $data );
+ $recipients = extract_recipients_from_activity( $data );
$users = array();
foreach ( $recipients as $recipient ) {
@@ -478,41 +355,4 @@ class Inbox {
return $users;
}
-
- /**
- * Check if passed Activity is Public
- *
- * @param array $data
- * @return boolean
- */
- public static function is_activity_public( $data ) {
- $recipients = self::extract_recipients( $data );
-
- return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
- }
-
- /**
- * Adds line breaks to the list of allowed comment tags.
- *
- * @param array $allowedtags Allowed HTML tags.
- * @param string $context Context.
- * @return array Filtered tag list.
- */
- public static function allowed_comment_html( $allowedtags, $context = '' ) {
- if ( 'pre_comment_content' !== $context ) {
- // Do nothing.
- return $allowedtags;
- }
-
- // Add `p` and `br` to the list of allowed tags.
- if ( ! array_key_exists( 'br', $allowedtags ) ) {
- $allowedtags['br'] = array();
- }
-
- if ( ! array_key_exists( 'p', $allowedtags ) ) {
- $allowedtags['p'] = array();
- }
-
- return $allowedtags;
- }
}
diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php
index bf89038..1bad5a7 100644
--- a/includes/rest/class-server.php
+++ b/includes/rest/class-server.php
@@ -110,14 +110,20 @@ class Server {
if ( 'GET' !== $request->get_method() ) {
$verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) {
- return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
+ return new WP_Error(
+ 'activitypub_signature_verification',
+ $verified_request->get_error_message(),
+ array( 'status' => 401 )
+ );
}
- } elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode
- if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
- $verified_request = Signature::verify_http_signature( $request );
- if ( \is_wp_error( $verified_request ) ) {
- return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
- }
+ } elseif ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) { // GET-Requests are only signed in secure mode
+ $verified_request = Signature::verify_http_signature( $request );
+ if ( \is_wp_error( $verified_request ) ) {
+ return new WP_Error(
+ 'activitypub_signature_verification',
+ $verified_request->get_error_message(),
+ array( 'status' => 401 )
+ );
}
}
diff --git a/includes/rest/class-webfinger.php b/includes/rest/class-webfinger.php
index 34ae392..52abd9f 100644
--- a/includes/rest/class-webfinger.php
+++ b/includes/rest/class-webfinger.php
@@ -72,7 +72,7 @@ class Webfinger {
$params['resource'] = array(
'required' => true,
'type' => 'string',
- 'pattern' => '^acct:(.+)@(.+)$',
+ 'pattern' => '^(acct:)|^(https?://)(.+)$',
);
return $params;
diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php
index 3045ddd..df9747b 100644
--- a/includes/table/class-followers.php
+++ b/includes/table/class-followers.php
@@ -5,6 +5,8 @@ use WP_List_Table;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers as FollowerCollection;
+use function Activitypub\object_to_uri;
+
if ( ! \class_exists( '\WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
@@ -101,7 +103,7 @@ class Followers extends WP_List_Table {
'icon' => esc_attr( $follower->get_icon_url() ),
'post_title' => esc_attr( $follower->get_name() ),
'username' => esc_attr( $follower->get_preferred_username() ),
- 'url' => esc_attr( $follower->get_url() ),
+ 'url' => esc_attr( object_to_uri( $follower->get_url() ) ),
'identifier' => esc_attr( $follower->get_id() ),
'published' => esc_attr( $follower->get_published() ),
'modified' => esc_attr( $follower->get_updated() ),
diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php
index cc7777d..480e7da 100644
--- a/includes/transformer/class-post.php
+++ b/includes/transformer/class-post.php
@@ -107,6 +107,32 @@ class Post extends Base {
return $object_type;
}
+ /**
+ * Gets the template to use to generate the content of the activitypub item.
+ *
+ * @return string The Template.
+ */
+ protected function get_post_content_template() {
+ $type = \get_option( 'activitypub_post_content_type', 'content' );
+
+ switch ( $type ) {
+ case 'excerpt':
+ $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
+ break;
+ case 'title':
+ $template = "[ap_title]\n\n[ap_permalink type=\"html\"]";
+ break;
+ case 'content':
+ $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
+ break;
+ default:
+ $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
+ break;
+ }
+
+ return apply_filters( 'activitypub_object_content_template', $template, $this->wp_post );
+ }
+
/**
* Transforms the WP_Post object to an ActivityPub Object
*
diff --git a/integration/class-webfinger.php b/integration/class-webfinger.php
index c9dd565..6b3b6dd 100644
--- a/integration/class-webfinger.php
+++ b/integration/class-webfinger.php
@@ -53,10 +53,12 @@ class Webfinger {
* @return array the jrd array
*/
public static function add_pseudo_user_discovery( $array, $resource ) {
- if ( $array ) {
+ $user = Webfinger_Rest::get_profile( $resource );
+
+ if ( ! $user || is_wp_error( $user ) ) {
return $array;
}
- return Webfinger_Rest::get_profile( $resource );
+ return $user;
}
}
diff --git a/readme.txt b/readme.txt
index 5ffb987..40f210a 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.4
-Stable tag: 1.2.0
+Stable tag: 1.3.0
Requires PHP: 5.6
License: MIT
License URI: http://opensource.org/licenses/MIT
@@ -105,9 +105,22 @@ 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).
-= 1.2.1 =
+= 2.0.0 =
+* Removed: Deprecated Classes
+* Fixed: Normalize attributes that can have mixed value types
+* Added: URL support for WebFinger
+* Added: Make Post-Template filterable
+* Addes: CSS class for ActivityPub comments to allow custom designs
+
+= 1.3.0 =
+
+* Added: Threaded-Comments support
* Improved: alt text for avatars in Follow Me/Followers blocks
+* Improved: `Delete`, `Update` and `Follow` Activities
+* Improved: better/more effective handling of `Delete` Activities
+* Improved: allow `` and `
` for Comments
+* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
= 1.2.0 =
diff --git a/tests/class-activitypub-testcase-cache-http.php b/tests/class-activitypub-testcase-cache-http.php
index b8b0e43..a75c092 100644
--- a/tests/class-activitypub-testcase-cache-http.php
+++ b/tests/class-activitypub-testcase-cache-http.php
@@ -12,7 +12,7 @@ class ActivityPub_TestCase_Cache_HTTP extends \WP_UnitTestCase {
add_filter(
'rest_url',
- function() {
+ function () {
return get_option( 'home' ) . '/wp-json/';
}
);
diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php
index b7534a0..c42d391 100644
--- a/tests/test-class-activitypub-activity-dispatcher.php
+++ b/tests/test-class-activitypub-activity-dispatcher.php
@@ -70,7 +70,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter(
'activitypub_extract_mentions',
- function( $mentions ) {
+ function ( $mentions ) {
$mentions[] = 'https://example.com/alex';
return $mentions;
},
@@ -134,7 +134,7 @@ class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HT
add_filter(
'activitypub_is_user_type_disabled',
- function( $value, $type ) {
+ function ( $value, $type ) {
if ( 'blog' === $type ) {
return false;
} else {
diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php
index 4748106..5b73f9d 100644
--- a/tests/test-class-activitypub-activity.php
+++ b/tests/test-class-activitypub-activity.php
@@ -1,4 +1,6 @@
assertEquals( 'Hello world!', $object->get_content() );
$this->assertEquals( $test_array, $object->to_array() );
}
+
+ public function test_activity_object() {
+ $test_array = array(
+ 'id' => 'https://example.com/post/123',
+ 'type' => 'Create',
+ 'object' => array(
+ 'id' => 'https://example.com/post/123/activity',
+ 'type' => 'Note',
+ 'content' => 'Hello world!',
+ ),
+ );
+
+ $activity = \Activitypub\Activity\Activity::init_from_array( $test_array );
+
+ $this->assertEquals( 'Hello world!', $activity->get_object()->get_content() );
+ Assert::assertArraySubset( $test_array, $activity->to_array() );
+ }
}
diff --git a/tests/test-class-activitypub-create-handler.php b/tests/test-class-activitypub-create-handler.php
new file mode 100644
index 0000000..95c5025
--- /dev/null
+++ b/tests/test-class-activitypub-create-handler.php
@@ -0,0 +1,70 @@
+user_id = 1;
+ $authordata = \get_userdata( $this->user_id );
+ $this->user_url = $authordata->user_url;
+
+ $this->post_id = \wp_insert_post(
+ array(
+ 'post_author' => $this->user_id,
+ 'post_content' => 'test',
+ )
+ );
+ $this->post_permalink = \get_permalink( $this->post_id );
+
+ \add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Create_Handler', 'get_remote_metadata_by_actor' ), 0, 2 );
+ }
+
+ public static function get_remote_metadata_by_actor( $value, $actor ) {
+ return array(
+ 'name' => 'Example User',
+ 'icon' => array(
+ 'url' => 'https://example.com/icon',
+ ),
+ 'url' => $actor,
+ 'id' => 'http://example.org/users/example',
+ );
+ }
+
+ public function create_test_object( $id = 'https://example.com/123' ) {
+ return array(
+ 'actor' => $this->user_url,
+ 'id' => 'https://example.com/id/' . microtime( true ),
+ 'to' => [ $this->user_url ],
+ 'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
+ 'object' => array(
+ 'id' => $id,
+ 'url' => 'https://example.com/example',
+ 'inReplyTo' => $this->post_permalink,
+ 'content' => 'example',
+ ),
+ );
+ }
+
+ public function test_handle_create_object_unset_rejected() {
+ $object = $this->create_test_object();
+ unset( $object['object'] );
+ $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
+ $this->assertNull( $converted );
+ }
+
+ public function test_handle_create_non_public_rejected() {
+ $object = $this->create_test_object();
+ $object['cc'] = [];
+ $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
+ $this->assertNull( $converted );
+ }
+
+ public function test_handle_create_no_id_rejected() {
+ $object = $this->create_test_object();
+ unset( $object['object']['id'] );
+ $converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
+ $this->assertNull( $converted );
+ }
+}
diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-activitypub-followers.php
similarity index 85%
rename from tests/test-class-db-activitypub-followers.php
rename to tests/test-class-activitypub-followers.php
index 8fc0068..8d5fb32 100644
--- a/tests/test-class-db-activitypub-followers.php
+++ b/tests/test-class-activitypub-followers.php
@@ -1,5 +1,5 @@
array(
'id' => 'https://example.org/users/username',
@@ -76,7 +76,7 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( 3, \count( $db_followers ) );
$db_followers = array_map(
- function( $item ) {
+ function ( $item ) {
return $item->get_url();
},
$db_followers
@@ -329,6 +329,82 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( $name, $follower->get_name() );
}
+ public function test_get_inboxes() {
+ for ( $i = 0; $i < 30; $i++ ) {
+ $meta = array(
+ 'id' => 'https://example.org/users/' . $i,
+ 'url' => 'https://example.org/users/' . $i,
+ 'inbox' => 'https://example.org/users/' . $i . '/inbox',
+ 'name' => 'user' . $i,
+ 'preferredUsername' => 'user' . $i,
+ 'publicKey' => 'https://example.org/users/' . $i . '#main-key',
+ 'publicKeyPem' => $i,
+ );
+
+ $follower = new \Activitypub\Model\Follower();
+ $follower->from_array( $meta );
+
+ $id = $follower->upsert();
+
+ add_post_meta( $id, 'activitypub_user_id', 1 );
+ }
+
+ $inboxes = \Activitypub\Collection\Followers::get_inboxes( 1 );
+
+ $this->assertCount( 30, $inboxes );
+
+ wp_cache_delete( sprintf( \Activitypub\Collection\Followers::CACHE_KEY_INBOXES, 1 ), 'activitypub' );
+
+ for ( $j = 0; $j < 5; $j++ ) {
+ $k = $j + 100;
+ $meta = array(
+ 'id' => 'https://example.org/users/' . $k,
+ 'url' => 'https://example.org/users/' . $k,
+ 'inbox' => 'https://example.org/users/' . $j . '/inbox',
+ 'name' => 'user' . $k,
+ 'preferredUsername' => 'user' . $k,
+ 'publicKey' => 'https://example.org/users/' . $k . '#main-key',
+ 'publicKeyPem' => $k,
+ );
+
+ $follower = new \Activitypub\Model\Follower();
+ $follower->from_array( $meta );
+
+ $id = $follower->upsert();
+
+ add_post_meta( $id, 'activitypub_user_id', 1 );
+ }
+
+ $inboxes2 = \Activitypub\Collection\Followers::get_inboxes( 1 );
+
+ $this->assertCount( 30, $inboxes2 );
+ }
+
+ public function test_get_all_followers() {
+ for ( $i = 0; $i < 30; $i++ ) {
+ $meta = array(
+ 'id' => 'https://example.org/users/' . $i,
+ 'url' => 'https://example.org/users/' . $i,
+ 'inbox' => 'https://example.org/users/' . $i . '/inbox',
+ 'name' => 'user' . $i,
+ 'preferredUsername' => 'user' . $i,
+ 'publicKey' => 'https://example.org/users/' . $i . '#main-key',
+ 'publicKeyPem' => $i,
+ );
+
+ $follower = new \Activitypub\Model\Follower();
+ $follower->from_array( $meta );
+
+ $id = $follower->upsert();
+
+ add_post_meta( $id, 'activitypub_user_id', 1 );
+ }
+
+ $followers = \Activitypub\Collection\Followers::get_all_followers();
+
+ $this->assertCount( 30, $followers );
+ }
+
public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
return true;
diff --git a/tests/test-class-activitypub-interactions.php b/tests/test-class-activitypub-interactions.php
new file mode 100644
index 0000000..6345095
--- /dev/null
+++ b/tests/test-class-activitypub-interactions.php
@@ -0,0 +1,174 @@
+user_id = 1;
+ $authordata = \get_userdata( $this->user_id );
+ $this->user_url = $authordata->user_url;
+
+ $this->post_id = \wp_insert_post(
+ array(
+ 'post_author' => $this->user_id,
+ 'post_content' => 'test',
+ )
+ );
+ $this->post_permalink = \get_permalink( $this->post_id );
+
+ \add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Interactions', 'get_remote_metadata_by_actor' ), 0, 2 );
+ }
+
+ public static function get_remote_metadata_by_actor( $value, $actor ) {
+ return array(
+ 'name' => 'Example User',
+ 'icon' => array(
+ 'url' => 'https://example.com/icon',
+ ),
+ 'url' => $actor,
+ 'id' => 'http://example.org/users/example',
+ );
+ }
+
+ public function create_test_object( $id = 'https://example.com/123' ) {
+ return array(
+ 'actor' => $this->user_url,
+ 'id' => 'https://example.com/id/' . microtime( true ),
+ 'to' => [ $this->user_url ],
+ 'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
+ 'object' => array(
+ 'id' => $id,
+ 'url' => 'https://example.com/example',
+ 'inReplyTo' => $this->post_permalink,
+ 'content' => 'example',
+ ),
+ );
+ }
+
+ public function create_test_rich_object( $id = 'https://example.com/123' ) {
+ return array(
+ 'actor' => $this->user_url,
+ 'id' => 'https://example.com/id/' . microtime( true ),
+ 'to' => [ $this->user_url ],
+ 'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
+ 'object' => array(
+ 'id' => $id,
+ 'url' => 'https://example.com/example',
+ 'inReplyTo' => $this->post_permalink,
+ 'content' => 'Hello
exampleexample
',
+ ),
+ );
+ }
+
+ public function test_handle_create_basic() {
+ $comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_object() );
+ $comment = get_comment( $comment_id, ARRAY_A );
+
+ $this->assertIsArray( $comment );
+ $this->assertEquals( $this->post_id, $comment['comment_post_ID'] );
+ $this->assertEquals( 'Example User', $comment['comment_author'] );
+ $this->assertEquals( $this->user_url, $comment['comment_author_url'] );
+ $this->assertEquals( 'example', $comment['comment_content'] );
+ $this->assertEquals( 'comment', $comment['comment_type'] );
+ $this->assertEquals( '', $comment['comment_author_email'] );
+ $this->assertEquals( 0, $comment['comment_parent'] );
+ $this->assertEquals( 'https://example.com/123', get_comment_meta( $comment_id, 'source_id', true ) );
+ $this->assertEquals( 'https://example.com/example', get_comment_meta( $comment_id, 'source_url', true ) );
+ $this->assertEquals( 'https://example.com/icon', get_comment_meta( $comment_id, 'avatar_url', true ) );
+ $this->assertEquals( 'activitypub', get_comment_meta( $comment_id, 'protocol', true ) );
+ }
+
+ public function test_handle_create_rich() {
+ $comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_rich_object() );
+ $comment = get_comment( $comment_id, ARRAY_A );
+
+ $this->assertEquals( 'Hello
exampleexample
', $comment['comment_content'] );
+
+ $commentarray = array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_author' => 'Example User',
+ 'comment_author_url' => $this->user_url,
+ 'comment_content' => 'Hello
exampleexample
',
+ 'comment_type' => 'comment',
+ 'comment_author_email' => '',
+ 'comment_parent' => 0,
+ 'comment_meta' => array(
+ 'source_id' => 'https://example.com/123',
+ 'source_url' => 'https://example.com/example',
+ 'protocol' => 'activitypub',
+ ),
+ );
+
+ \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
+ $comment_id = wp_new_comment( $commentarray );
+ \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
+ $comment = get_comment( $comment_id, ARRAY_A );
+
+ $this->assertEquals( 'Helloexampleexample', $comment['comment_content'] );
+ }
+
+ public function test_convert_object_to_comment_not_reply_rejected() {
+ $object = $this->create_test_object();
+ unset( $object['object']['inReplyTo'] );
+ $converted = Activitypub\Collection\Interactions::add_comment( $object );
+ $this->assertFalse( $converted );
+ }
+
+ public function test_convert_object_to_comment_already_exists_rejected() {
+ $object = $this->create_test_object( 'https://example.com/test_convert_object_to_comment_already_exists_rejected' );
+ Activitypub\Collection\Interactions::add_comment( $object );
+ $converted = Activitypub\Collection\Interactions::add_comment( $object );
+ $this->assertEquals( $converted->get_error_code(), 'comment_duplicate' );
+ }
+
+ public function test_convert_object_to_comment_reply_to_comment() {
+ $id = 'https://example.com/test_convert_object_to_comment_reply_to_comment';
+ $object = $this->create_test_object( $id );
+ Activitypub\Collection\Interactions::add_comment( $object );
+ $comment = \Activitypub\object_id_to_comment( $id );
+
+ $object['object']['inReplyTo'] = $id;
+ $object['object']['id'] = 'https://example.com/234';
+ $id = Activitypub\Collection\Interactions::add_comment( $object );
+ $converted = get_comment( $id, ARRAY_A );
+
+ $this->assertIsArray( $converted );
+ $this->assertEquals( $this->post_id, $converted['comment_post_ID'] );
+ $this->assertEquals( $comment->comment_ID, $converted['comment_parent'] );
+ }
+
+ public function test_convert_object_to_comment_reply_to_non_existent_comment_rejected() {
+ $object = $this->create_test_object();
+ $object['object']['inReplyTo'] = 'https://example.com/not_found';
+ $converted = Activitypub\Collection\Interactions::add_comment( $object );
+ $this->assertFalse( $converted );
+ }
+
+ public function test_handle_create_basic2() {
+ $id = 'https://example.com/test_handle_create_basic';
+ $object = $this->create_test_object( $id );
+ Activitypub\Collection\Interactions::add_comment( $object );
+ $comment = \Activitypub\object_id_to_comment( $id );
+ $this->assertInstanceOf( WP_Comment::class, $comment );
+ }
+
+ public function test_get_interaction_by_id() {
+ $id = 'https://example.com/test_get_interaction_by_id';
+ $url = 'https://example.com/test_get_interaction_by_url';
+ $object = $this->create_test_object( $id );
+ $object['object']['url'] = $url;
+
+ Activitypub\Collection\Interactions::add_comment( $object );
+ $comment = \Activitypub\object_id_to_comment( $id );
+ $interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $id );
+ $this->assertIsArray( $interactions );
+ $this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
+
+ $comment = \Activitypub\object_id_to_comment( $id );
+ $interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $url );
+ $this->assertIsArray( $interactions );
+ $this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
+ }
+}
diff --git a/tests/test-class-activitypub-rest-inbox.php b/tests/test-class-activitypub-rest-inbox.php
index 58f16f3..0368d5b 100644
--- a/tests/test-class-activitypub-rest-inbox.php
+++ b/tests/test-class-activitypub-rest-inbox.php
@@ -5,7 +5,7 @@ class Test_Activitypub_Rest_Inbox extends WP_UnitTestCase {
*/
public function test_is_activity_public( $data, $check ) {
- $this->assertEquals( $check, Activitypub\Rest\Inbox::is_activity_public( $data ) );
+ $this->assertEquals( $check, Activitypub\is_activity_public( $data ) );
}
public function the_data_provider() {
diff --git a/tests/test-class-activitypub-rest-post-signature-verification.php b/tests/test-class-activitypub-rest-post-signature-verification.php
index 98e7855..12b0abe 100644
--- a/tests/test-class-activitypub-rest-post-signature-verification.php
+++ b/tests/test-class-activitypub-rest-post-signature-verification.php
@@ -55,7 +55,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
public function test_rest_activity_signature() {
add_filter(
'pre_get_remote_metadata_by_actor',
- function( $json, $actor ) {
+ function ( $json, $actor ) {
$user = Activitypub\Collection\Users::get_by_id( 1 );
$public_key = Activitypub\Signature::get_public_key_for( $user->get__id() );
// return ActivityPub Profile with signature
diff --git a/tests/test-class-activitypub-users-collection.php b/tests/test-class-activitypub-users-collection.php
new file mode 100644
index 0000000..a5fd60c
--- /dev/null
+++ b/tests/test-class-activitypub-users-collection.php
@@ -0,0 +1,43 @@
+assertInstanceOf( $expected, $user );
+ }
+
+ public function the_resource_provider() {
+ return array(
+ array( 'http://example.org/?author=1', 'Activitypub\Model\User' ),
+ array( 'https://example.org/?author=1', 'Activitypub\Model\User' ),
+ array( 'http://example.org/?author=7', 'WP_Error' ),
+ array( 'acct:admin@example.org', 'Activitypub\Model\User' ),
+ array( 'acct:blog@example.org', 'Activitypub\Model\Blog_User' ),
+ array( 'acct:*@example.org', 'Activitypub\Model\Blog_User' ),
+ array( 'acct:_@example.org', 'Activitypub\Model\Blog_User' ),
+ array( 'acct:aksd@example.org', 'WP_Error' ),
+ array( 'admin@example.org', 'Activitypub\Model\User' ),
+ array( 'acct:application@example.org', 'Activitypub\Model\Application_User' ),
+ array( 'http://example.org/@admin', 'Activitypub\Model\User' ),
+ array( 'http://example.org/@blog', 'Activitypub\Model\Blog_User' ),
+ array( 'https://example.org/@blog', 'Activitypub\Model\Blog_User' ),
+ array( 'http://example.org/@blog/', 'Activitypub\Model\Blog_User' ),
+ array( 'http://example.org/', 'Activitypub\Model\Blog_User' ),
+ array( 'http://example.org', 'Activitypub\Model\Blog_User' ),
+ array( 'https://example.org/', 'Activitypub\Model\Blog_User' ),
+ array( 'https://example.org', 'Activitypub\Model\Blog_User' ),
+ array( 'http://example.org/@blog/s', 'WP_Error' ),
+ array( 'http://example.org/@blogs/', 'WP_Error' ),
+ );
+ }
+}
diff --git a/tests/test-functions.php b/tests/test-functions.php
index 68140e0..4ee049c 100644
--- a/tests/test-functions.php
+++ b/tests/test-functions.php
@@ -1,9 +1,149 @@
assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
}
+
+ public function set_up() {
+ $this->post_id = \wp_insert_post(
+ array(
+ 'post_author' => $this->user_id,
+ 'post_content' => 'test',
+ )
+ );
+ }
+
+ public function test_object_id_to_comment_basic() {
+ $single_comment_source_id = 'https://example.com/single';
+ $content = 'example';
+ $comment_id = \wp_new_comment(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_author' => 'Example User',
+ 'comment_author_url' => 'https://example.com/user',
+ 'comment_content' => $content,
+ 'comment_type' => '',
+ 'comment_author_email' => '',
+ 'comment_parent' => 0,
+ 'comment_meta' => array(
+ 'source_id' => $single_comment_source_id,
+ 'source_url' => 'https://example.com/123',
+ 'avatar_url' => 'https://example.com/icon',
+ 'protocol' => 'activitypub',
+ ),
+ ),
+ true
+ );
+ $query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
+ $this->assertInstanceOf( WP_Comment::class, $query_result );
+ $this->assertEquals( $comment_id, $query_result->comment_ID );
+ $this->assertEquals( $content, $query_result->comment_content );
+ }
+
+ public function test_object_id_to_comment_none() {
+ $single_comment_source_id = 'https://example.com/none';
+ $query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
+ $this->assertFalse( $query_result );
+ }
+
+ public function test_object_id_to_comment_duplicate() {
+ $duplicate_comment_source_id = 'https://example.com/duplicate';
+ for ( $i = 0; $i < 2; ++$i ) {
+ \wp_new_comment(
+ array(
+ 'comment_post_ID' => $this->post_id,
+ 'comment_author' => 'Example User',
+ 'comment_author_url' => 'https://example.com/user',
+ 'comment_content' => 'example',
+ 'comment_type' => '',
+ 'comment_author_email' => '',
+ 'comment_parent' => 0,
+ 'comment_meta' => array(
+ 'source_id' => $duplicate_comment_source_id,
+ 'source_url' => 'https://example.com/123',
+ 'avatar_url' => 'https://example.com/icon',
+ 'protocol' => 'activitypub',
+ ),
+ ),
+ true
+ );
+ }
+ $query_result = \Activitypub\object_id_to_comment( $duplicate_comment_source_id );
+ $this->assertFalse( $query_result );
+ }
+
+ /**
+ * @dataProvider object_to_uri_provider
+ */
+ public function test_object_to_uri( $input, $output ) {
+ $this->assertEquals( $output, \Activitypub\object_to_uri( $input ) );
+ }
+
+ public function object_to_uri_provider() {
+ return array(
+ array( null, null ),
+ array( 'https://example.com', 'https://example.com' ),
+ array( array( 'https://example.com' ), 'https://example.com' ),
+ array(
+ array(
+ 'https://example.com',
+ 'https://example.org',
+ ),
+ 'https://example.com',
+ ),
+ array(
+ array(
+ 'type' => 'Link',
+ 'href' => 'https://example.com',
+ ),
+ 'https://example.com',
+ ),
+ array(
+ array(
+ array(
+ 'type' => 'Link',
+ 'href' => 'https://example.com',
+ ),
+ array(
+ 'type' => 'Link',
+ 'href' => 'https://example.org',
+ ),
+ ),
+ 'https://example.com',
+ ),
+ array(
+ array(
+ 'type' => 'Actor',
+ 'id' => 'https://example.com',
+ ),
+ 'https://example.com',
+ ),
+ array(
+ array(
+ array(
+ 'type' => 'Actor',
+ 'id' => 'https://example.com',
+ ),
+ array(
+ 'type' => 'Actor',
+ 'id' => 'https://example.org',
+ ),
+ ),
+ 'https://example.com',
+ ),
+ array(
+ array(
+ 'type' => 'Activity',
+ 'id' => 'https://example.com',
+ ),
+ 'https://example.com',
+ ),
+ );
+ }
}