diff --git a/activitypub.php b/activitypub.php index 8f665fc..75d52b1 100644 --- a/activitypub.php +++ b/activitypub.php @@ -29,6 +29,9 @@ 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__ ) ); @@ -39,6 +42,7 @@ function init() { Collection\Followers::init(); // Configure the REST API route + Rest\Users::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..a7744ce 100644 --- a/assets/css/activitypub-admin.css +++ b/assets/css/activitypub-admin.css @@ -1,9 +1,18 @@ +.activitypub-settings-body { + max-width: 800px; + margin: 0 auto; +} + .settings_page_activitypub .notice { max-width: 800px; margin: auto; margin-top: 10px; } +.settings_page_activitypub .wrap { + padding-left: 22px; +} + .activitypub-settings-header { text-align: center; margin: 0 0 1rem; @@ -25,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; + grid-template-columns: auto auto auto; } .activitypub-settings-tab.active { @@ -111,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; @@ -127,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; } @@ -139,3 +150,29 @@ summary { outline: 2px solid #2271b1; background-color: #f6f7f7; } + +.activitypub-settings-body +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/activity/class-activity.php b/includes/activity/class-activity.php new file mode 100644 index 0000000..bd13cf8 --- /dev/null +++ b/includes/activity/class-activity.php @@ -0,0 +1,207 @@ + '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 + * | Base_Objectr + * | 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; + + /** + * 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 string|Base_Objectr|Link|null $object + * + * @return void + */ + 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 ) ); + } + + 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->get_attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->get_attributed_to() ); + } + + if ( $object->get_id() && ! $this->get_id() ) { + $this->set( 'id', $object->get_id() . '#activity' ); + } + } +} diff --git a/includes/activity/class-actor.php b/includes/activity/class-actor.php index 604261f..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 @@ -114,4 +122,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..9c5f52a 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 */ @@ -462,6 +463,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. * @@ -472,7 +491,6 @@ class Base_Object { public function get( $key ) { if ( ! $this->has( $key ) ) { return new WP_Error( 'invalid_key', 'Invalid key' ); - } return $this->$key; @@ -537,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 ); } /** @@ -550,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 ) { @@ -563,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. * @@ -576,17 +617,50 @@ 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 ( ! $value ) { + if ( ! isset( $value ) ) { $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 ( $value ) { + if ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; } } + // 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() ); + + $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; } + + /** + * Convert Object to JSON. + * + * @return string The JSON string. + */ + public function to_json() { + $array = $this->to_array(); + + return \wp_json_encode( $array, \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); + } } 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 @@ -post_author = Users::BLOG_USER_ID; - /** - * Send "delete" activities. - * - * @param Activitypub\Model\Post $activitypub_post The ActivityPub Post. - */ - public static function send_delete_activity( Post $activitypub_post ) { - self::send_activity( $activitypub_post, 'Delete' ); + if ( is_single_user() ) { + self::send_activity( $wp_post, $type ); + } else { + self::send_announce( $wp_post, $type ); + } } /** * Send Activities to followers and mentioned users. * - * @param Activitypub\Model\Post $activitypub_post The ActivityPub Post. - * @param string $activity_type The Activity-Type. + * @param WP_Post $wp_post The ActivityPub Post. + * @param string $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 - Migration::maybe_migrate(); + public static function send_activity( WP_Post $wp_post, $type ) { + if ( is_user_disabled( $wp_post->post_author ) ) { + return; + } - // get latest version of post - $user_id = $activitypub_post->get_post_author(); + $object = Post::transform( $wp_post )->to_object(); - $activitypub_activity = new Activity( $activity_type ); - $activitypub_activity->from_post( $activitypub_post ); + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_object( $object ); - $follower_inboxes = Followers::get_inboxes( $user_id ); - $mentioned_inboxes = Mention::get_inboxes( $activitypub_activity->get_cc() ); + $follower_inboxes = Followers::get_inboxes( $wp_post->post_author ); + $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(); + $json = $activity->to_json(); - safe_remote_post( $inbox, $activity, $user_id ); + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $wp_post->post_author ); + } + } + + /** + * Send Announces 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_announce( 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; + } + + $object = Post::transform( $wp_post )->to_object(); + + $activity = new Activity(); + $activity->set_type( 'Announce' ); + // to pre-fill attributes like "published" and "id" + $activity->set_object( $object ); + // send only the id + $activity->set_object( $object->get_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 ); + $inboxes = array_unique( $inboxes ); + + $json = $activity->to_json(); + + foreach ( $inboxes as $inbox ) { + safe_remote_post( $inbox, $json, $wp_post->post_author ); } } } diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php index 55329b3..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 @@ -28,6 +29,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 ); } /** @@ -69,15 +72,18 @@ 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 ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return $template; } - // Ensure that edge caches know that this page can deliver both HTML and JSON. - header( 'Vary: Accept' ); + if ( ! is_activitypub_request() ) { + return $template; + } + + $json_template = false; // check if user can publish posts - if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) { + if ( \is_author() && ! Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) { return $template; } @@ -89,18 +95,15 @@ 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; } - return $template; + return $json_template; } /** @@ -224,6 +227,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 ); @@ -236,4 +244,36 @@ class Activitypub { self::add_rewrite_rules(); \flush_rewrite_rules(); } + + /** + * Theme compatibility stuff + * + * @return void + */ + 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/includes/class-admin.php b/includes/class-admin.php index 7b62a08..647805a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -1,8 +1,6 @@ \__( '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', @@ -118,7 +129,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', @@ -143,6 +158,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-http.php b/includes/class-http.php index 58551a4..1157137 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\Collection\Users; /** * ActivityPub HTTP Class @@ -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, @@ -63,10 +69,17 @@ 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( 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 d3c0fcb..1ff8b66 100644 --- a/includes/class-migration.php +++ b/includes/class-migration.php @@ -1,7 +1,7 @@ 'ID' ) ) as $user_id ) { $followers = get_user_meta( $user_id, 'activitypub_followers', true ); if ( $followers ) { foreach ( $followers as $actor ) { - $meta = get_remote_metadata_by_actor( $actor ); - - if ( is_tombstone( $meta ) ) { - continue; - } - - $follower = Follower::from_array( $meta ); - - if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - $follower->set_error( $meta ); - } - - $follower->upsert(); - - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + Followers::add_follower( $user_id, $actor ); } } } + + // 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/class-scheduler.php b/includes/class-scheduler.php index d55fb35..d77a132 100644 --- a/includes/class-scheduler.php +++ b/includes/class-scheduler.php @@ -2,8 +2,9 @@ namespace Activitypub; -use Activitypub\Model\Post; +use Activitypub\Collection\Users; use Activitypub\Collection\Followers; +use Activitypub\Transformer\Post; /** * ActivityPub Scheduler Class @@ -68,27 +69,34 @@ class Scheduler { return; } - $activitypub_post = new Post( $post ); + $type = false; if ( 'publish' === $new_status && 'publish' !== $old_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_create_activity', - array( $activitypub_post ) - ); + $type = 'Create'; } elseif ( 'publish' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_activity', - array( $activitypub_post ) - ); + $type = 'Update'; } elseif ( 'trash' === $new_status ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_delete_activity', - array( $activitypub_post ) - ); + $type = 'Delete'; } + + if ( ! $type ) { + return; + } + + \wp_schedule_single_event( + \time(), + 'activitypub_send_activity', + array( $post, $type ) + ); + + \wp_schedule_single_event( + \time(), + sprintf( + 'activitypub_send_%s_activity', + \strtolower( $type ) + ), + array( $post ) + ); } /** @@ -103,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(); } } @@ -129,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/class-signature.php b/includes/class-signature.php index a3293d6..b7c98fc 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\Collection\Users; /** * 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 = Users::get_by_id( $user_id ); + $key = $user->get__private_key(); $url_parts = \wp_parse_url( $url ); @@ -143,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 = Users::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 ); @@ -252,7 +242,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-webfinger.php b/includes/class-webfinger.php index 1581853..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 @@ -24,26 +25,33 @@ class Webfinger { return \get_webfinger_resource( $user_id, false ); } - $user = \get_user_by( 'id', $user_id ); + $user = Users::get_by_id( $user_id ); if ( ! $user ) { return ''; } - return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST ); + return $user->get_resource(); } - 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 +90,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/collection/class-followers.php b/includes/collection/class-followers.php index 4f7022a..f5527f1 100644 --- a/includes/collection/class-followers.php +++ b/includes/collection/class-followers.php @@ -6,8 +6,10 @@ use Exception; use WP_Query; use Activitypub\Http; use Activitypub\Webfinger; -use Activitypub\Model\Activity; 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; @@ -62,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, @@ -102,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, @@ -128,7 +90,7 @@ class Followers { register_post_meta( self::POST_TYPE, - '_actor', + 'activitypub_user_id', array( 'type' => 'string', 'single' => false, @@ -138,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' ); } @@ -191,17 +165,33 @@ 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; } - $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 ( is_array( $meta ) && ! in_array( $user_id, $meta, true ) ) { - add_post_meta( $follower->get__id(), '_user_id', $user_id ); + if ( $error ) { + self::add_error( $follower->get__id(), $error ); + } + + // 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' ); } @@ -225,7 +215,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 ); } /** @@ -241,7 +231,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 = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -252,7 +242,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; @@ -277,20 +267,25 @@ 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( '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 ); } @@ -304,16 +299,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 = -1, $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, ), ), @@ -321,10 +316,11 @@ 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 ) { - $items[] = Follower::from_custom_post_type( $post ); // phpcs:ignore + foreach ( $posts as $post ) { + $items[] = Follower::init_from_cpt( $post ); // phpcs:ignore } return $items; @@ -337,11 +333,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 ); } /** @@ -358,7 +354,7 @@ class Followers { 'fields' => 'ids', 'meta_query' => array( array( - 'key' => '_user_id', + 'key' => 'activitypub_user_id', 'value' => $user_id, ), ), @@ -390,11 +386,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, ), ), @@ -412,7 +408,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 ) @@ -452,7 +448,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; @@ -472,7 +468,7 @@ class Followers { 'posts_per_page' => $number, 'meta_query' => array( array( - 'key' => 'errors', + 'key' => 'activitypub_errors', 'compare' => 'EXISTS', ), ), @@ -482,9 +478,40 @@ 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; } + + /** + * This function is used to store errors that occur when + * sending an ActivityPub message to a Follower. + * + * 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 ) ) { + $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/collection/class-users.php b/includes/collection/class-users.php new file mode 100644 index 0000000..506722b --- /dev/null +++ b/includes/collection/class-users.php @@ -0,0 +1,187 @@ + 404 ) + ); + } + + 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( + 'activitypub_user_not_found', + \__( 'User not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + /** + * 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 ( Blog_User::get_default_username() === $username ) { + 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 ); + } + + // 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_user_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 = 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( + '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 ); + } + } + + /** + * Normalize the host. + * + * @param string $host The host. + * + * @return string The normalized host. + */ + public static function normalize_host( $host ) { + return \str_replace( 'www.', '', $host ); + } +} diff --git a/includes/functions.php b/includes/functions.php index 2a17bfa..9d2a03e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -1,43 +1,27 @@ '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 = Activity::CONTEXT; return \apply_filters( 'activitypub_json_context', $context ); } function safe_remote_post( $url, $body, $user_id ) { - return \Activitypub\Http::post( $url, $body, $user_id ); + return Http::post( $url, $body, $user_id ); } function safe_remote_get( $url ) { - return \Activitypub\Http::get( $url ); + return Http::get( $url ); } /** @@ -76,9 +60,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 ) { @@ -125,7 +110,7 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) { * @return array The followers. */ function get_followers( $user_id ) { - return Collection\Followers::get_followers( $user_id ); + return Followers::get_followers( $user_id ); } /** @@ -136,7 +121,7 @@ function get_followers( $user_id ) { * @return int The number of followers. */ function count_followers( $user_id ) { - return Collection\Followers::count_followers( $user_id ); + return Followers::count_followers( $user_id ); } /** @@ -187,21 +172,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 * @@ -271,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() ) { + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) { return false; } @@ -302,3 +272,83 @@ function is_activitypub_request() { return false; } + +/** + * This function checks if a user is disabled for ActivityPub. + * + * @param int $user_id The User-ID. + * + * @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; + 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; + break; + } + + $return = false; + 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; + } + + if ( ! \user_can( $user_id, 'publish_posts' ) ) { + $return = true; + break; + } + + $return = false; + break; + } + + return apply_filters( 'activitypub_is_user_disabled', $return, $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() { + $return = false; + + if ( + false === ACTIVITYPUB_DISABLE_BLOG_USER && + true === ACTIVITYPUB_DISABLE_USER + ) { + $return = true; + } + + return apply_filters( 'activitypub_is_single_user', $return ); +} + +if ( ! function_exists( 'get_self_link' ) ) { + /** + * Returns the link for the currently displayed feed. + * + * @return string Correct link for the atom:self element. + */ + 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-activity.php b/includes/model/class-activity.php deleted file mode 100644 index bf06bc1..0000000 --- a/includes/model/class-activity.php +++ /dev/null @@ -1,241 +0,0 @@ - '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 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-application-user.php b/includes/model/class-application-user.php new file mode 100644 index 0000000..888c709 --- /dev/null +++ b/includes/model/class-application-user.php @@ -0,0 +1,101 @@ +generate_key_pair(); + + $key = \get_option( 'activitypub_application_user_public_key' ); + + return $key; + } + + /** + * @param int $user_id + * + * @return mixed + */ + public function get__private_key() { + $key = \get_option( 'activitypub_application_user_private_key' ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + 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'] ); + \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 new file mode 100644 index 0000000..32d167a --- /dev/null +++ b/includes/model/class-blog-user.php @@ -0,0 +1,222 @@ + 404 ) + ); + } + + $object = new static(); + $object->_id = $user_id; + + return $object; + } + + /** + * Get the User-Name. + * + * @return string The User-Name. + */ + public function get_name() { + return \esc_html( \get_bloginfo( 'name' ) ); + } + + /** + * Get the User-Description. + * + * @return string The User-Description. + */ + public function get_summary() { + return \wpautop( + \wp_kses( + \get_bloginfo( 'description' ), + 'default' + ) + ); + } + + /** + * Get the User-Url. + * + * @return string The User-Url. + */ + public function get_url() { + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() ); + } + + /** + * Generate a default Username. + * + * @return string The auto-generated Username. + */ + public static function get_default_username() { + // check if domain host has a subdomain + $host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST ); + $host = \preg_replace( '/^www\./i', '', $host ); + + /** + * Filter the default blog username. + * + * @param string $host The default username. + */ + return apply_filters( 'activitypub_default_blog_username', $host ); + } + + /** + * Get the preferred User-Name. + * + * @return string The User-Name. + */ + public function get_preferred_username() { + $username = \get_option( 'activitypub_blog_user_identifier' ); + + if ( $username ) { + return $username; + } + + 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' ) ); + + if ( $image ) { + return array( + 'type' => 'Image', + 'url' => esc_url( $image[0] ), + ); + } + + 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( + 'type' => 'Image', + 'url' => esc_url( \get_header_image() ), + ); + } + + return null; + } + + public function get_published() { + $first_post = new WP_Query( + array( + 'orderby' => '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 ); + } + + public function get__public_key() { + $key = \get_option( 'activitypub_blog_user_public_key' ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + $key = \get_option( 'activitypub_blog_user_public_key' ); + + return $key; + } + + /** + * Get the User-Private-Key. + * + * @param int $user_id + * + * @return mixed + */ + public function get__private_key() { + $key = \get_option( 'activitypub_blog_user_private_key' ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + 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'] ); + \update_option( 'activitypub_blog_user_private_key', $key_pair['private_key'] ); + } + } + + public function get_attachment() { + return array(); + } + + 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; + } + } +} diff --git a/includes/model/class-follower.php b/includes/model/class-follower.php index e95aff8..39018e2 100644 --- a/includes/model/class-follower.php +++ b/includes/model/class-follower.php @@ -24,69 +24,29 @@ 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 - - /** - * 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 - * - * @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; + 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; } - $this->_errors = get_post_meta( $this->_id, 'errors' ); - return $this->_errors; + return $this->id; } /** @@ -95,7 +55,7 @@ class Follower extends Actor { * @return void */ public function reset_errors() { - delete_post_meta( $this->_id, 'errors' ); + delete_post_meta( $this->_id, 'activitypub_errors' ); } /** @@ -104,7 +64,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 ); @@ -113,21 +73,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 ); @@ -151,15 +103,32 @@ 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_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_status' => 'publish', + 'meta_input' => $this->get_post_meta_input(), ); $post_id = wp_insert_post( $args ); @@ -172,16 +141,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() { @@ -194,27 +164,9 @@ 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'] = $this->get_shared_inbox(); + $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; } @@ -239,37 +191,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; } /** @@ -279,16 +212,11 @@ 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 ) { + $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_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/model/class-user.php b/includes/model/class-user.php index 5c62473..23cd013 100644 --- a/includes/model/class-user.php +++ b/includes/model/class-user.php @@ -1,23 +1,258 @@ 404 ) + ); + } + + $object = new static(); + $object->_id = $user_id; + + return $object; + } + + /** + * 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->_id ) ); + } + + /** + * Get the User-Description. + * + * @return string The User-Description. + */ + public function get_summary() { + $description = get_user_meta( $this->_id, 'activitypub_user_description', true ); + if ( empty( $description ) ) { + $description = get_user_meta( $this->_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->_id ) ); + } + + public function get_at_url() { + return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() ); + } + + public function get_preferred_username() { + return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); + } + + public function get_icon() { + $icon = \esc_url( + \get_avatar_url( + $this->_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; + } + + public function get_published() { + return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); + } + + 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 ) { + return $key; + } + + $this->generate_key_pair(); + + return \get_user_meta( $this->get__id(), 'magic_sig_public_key', true ); + } + + /** + * @param int $this->get__id() + * + * @return mixed + */ + public function get__private_key() { + $key = \get_user_meta( $this->get__id(), 'magic_sig_private_key', true ); + + if ( $key ) { + return $key; + } + + $this->generate_key_pair(); + + 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__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 ); + } + } + + /** + * Returns the Inbox-API-Endpoint. + * + * @return string The Inbox-Endpoint. + */ + public function get_inbox() { + return get_rest_url_by_path( sprintf( 'users/%d/inbox', $this->get__id() ) ); + } + + /** + * 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. + * + * @return array The extended User-Output. + */ + public function get_attachment() { + $array = array(); + + $array[] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Blog', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + $array[] = array( + 'type' => 'PropertyValue', + 'name' => \__( 'Profile', 'activitypub' ), + 'value' => \html_entity_decode( + '' . \wp_parse_url( \get_author_posts_url( $this->get__id() ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + + 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', $this->get__id() ), \PHP_URL_HOST ) . '', + \ENT_QUOTES, + 'UTF-8' + ), + ); + } + + return $array; + } + + 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/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 30509be..cdff551 100644 --- a/includes/rest/class-followers.php +++ b/includes/rest/class-followers.php @@ -5,7 +5,8 @@ use WP_Error; use stdClass; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Collection\Followers as FollowerCollection; +use Activitypub\Collection\Users as User_Collection; +use Activitypub\Collection\Followers as Follower_Collection; 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,21 +52,14 @@ class Followers { */ public static function get( $request ) { $user_id = $request->get_param( 'user_id' ); - $user = \get_user_by( 'ID', $user_id ); + $user = User_Collection::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; } + $page = $request->get_param( 'page', 1 ); + /* * Action triggerd prior to the ActivityPub profile being created and sent to the client */ @@ -77,18 +71,29 @@ 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->first = $json->partOf; // phpcs:ignore - $json->totalItems = FollowerCollection::count_followers( $user_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 + $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 + } + + 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 ) { return $item->get_url(); }, - FollowerCollection::get_followers( $user_id ) + Follower_Collection::get_followers( $user->get__id(), 20, $page ) ); $response = new WP_REST_Response( $json, 200 ); @@ -107,14 +112,12 @@ class Followers { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $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..29e7e07 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_Collection::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__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..98b4e55 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\Signature; -use Activitypub\Model\Activity; +use Activitypub\Activity\Activity; +use Activitypub\Collection\Users as User_Collection; use function Activitypub\get_context; use function Activitypub\url_to_authorid; @@ -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_Collection::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__id() ) ); // phpcs:ignore $json->totalItems = 0; // phpcs:ignore @@ -118,15 +124,19 @@ 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 ); + + 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__id(), $type ); + \do_action( "activitypub_inbox_{$type}", $data, $user->get__id() ); return new WP_REST_Response( array(), 202 ); } @@ -139,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 ); @@ -185,10 +194,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 +214,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..047ae22 100644 --- a/includes/rest/class-outbox.php +++ b/includes/rest/class-outbox.php @@ -5,8 +5,9 @@ use stdClass; use WP_Error; use WP_REST_Server; use WP_REST_Response; -use Activitypub\Model\Post; -use Activitypub\Model\Activity; +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; @@ -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,23 +53,15 @@ 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_Collection::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; } - $page = $request->get_param( 'page', 0 ); + $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); + + $page = $request->get_param( 'page', 1 ); /* * Action triggerd prior to the ActivityPub profile being created and sent to the client @@ -80,14 +73,11 @@ 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->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 @@ -100,22 +90,28 @@ 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, ) ); 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 } } @@ -144,14 +140,12 @@ class Outbox { $params['page'] = array( 'type' => 'integer', + 'default' => 1, ); $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-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/includes/rest/class-users.php b/includes/rest/class-users.php new file mode 100644 index 0000000..b4f85fe --- /dev/null +++ b/includes/rest/class-users.php @@ -0,0 +1,102 @@ +[\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_Collection::get_by_various( $user_id ); + + if ( is_wp_error( $user ) ) { + return $user; + } + + // redirect to canonical URL if it is not an ActivityPub request + if ( ! is_activitypub_request() ) { + header( 'Location: ' . $user->get_canonical_url(), true, 301 ); + exit; + } + + /* + * Action triggerd prior to the ActivityPub profile being created and sent to the client + */ + \do_action( 'activitypub_outbox_pre' ); + + $user->set_context( + Activity::CONTEXT + ); + + $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..8e651d0 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\Collection\Users as User_Collection; /** * ActivityPub WebFinger REST-Class @@ -13,15 +14,20 @@ use WP_REST_Response; */ 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' ) ); - \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 ); } /** - * Register routes + * Register routes. + * + * @return void */ public static function register_routes() { \register_rest_route( @@ -39,54 +45,17 @@ 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' ); + $response = self::get_profile( $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 ) ); - } - - $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 ) ); - } - - $json = array( - 'subject' => $resource, - 'aliases' => array( - \get_author_posts_url( $user->ID ), - ), - 'links' => array( - array( - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => \get_author_posts_url( $user->ID ), - ), - array( - 'rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => \get_author_posts_url( $user->ID ), - ), - ), - ); - - return new WP_REST_Response( $json, 200 ); + return new WP_REST_Response( $response, 200 ); } /** @@ -112,14 +81,73 @@ 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_webfinger_discovery( $array, $resource, $user ) { + 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; } + + /** + * Add WebFinger discovery links + * + * @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 ) { + return $array; + } + + 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 ); + + 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; + } } diff --git a/includes/table/class-followers.php b/includes/table/class-followers.php index 246cc0b..b27f311 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\Collection\Users; use Activitypub\Collection\Followers as FollowerCollection; if ( ! \class_exists( '\WP_List_Table' ) ) { @@ -9,13 +10,31 @@ 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 = Users::BLOG_USER_ID; + } 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' => '', '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' ), @@ -36,14 +55,14 @@ 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() ); + $followers = FollowerCollection::get_followers( $this->user_id, $per_page, $page_num ); + $counter = FollowerCollection::count_followers( $this->user_id ); $this->items = array(); $this->set_pagination_args( array( 'total_items' => $counter, - 'total_pages' => round( $counter / $per_page ), + 'total_pages' => ceil( $counter / $per_page ), 'per_page' => $per_page, ) ); @@ -53,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(), @@ -83,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'] ); } @@ -104,7 +124,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; } @@ -116,9 +136,13 @@ 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; } } + + public function get_user_count() { + return FollowerCollection::count_followers( $this->user_id ); + } } diff --git a/includes/model/class-post.php b/includes/transformer/class-post.php similarity index 59% rename from includes/model/class-post.php rename to includes/transformer/class-post.php index 8e58a07..568a0e3 100644 --- a/includes/model/class-post.php +++ b/includes/transformer/class-post.php @@ -1,83 +1,39 @@ array( 'href' => array(), 'title' => array(), @@ -125,157 +81,96 @@ 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 - */ - public function __construct( $post ) { - $this->post = \get_post( $post ); - $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 static( $wp_post ); } /** - * Converts this Object into an Array. * - * @return array + * + * @param WP_Post $wp_post */ - public function to_array() { - $post = $this->post; + public function __construct( WP_Post $wp_post ) { + $this->wp_post = $wp_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 ), - 'summary' => $this->get_summary(), - 'inReplyTo' => null, - 'content' => $this->get_content(), - 'contentMap' => array( + /** + * Transforms the WP_Post object to an ActivityPub Object + * + * @see \Activitypub\Activity\Base_Object + * + * @return \Activitypub\Activity\Base_Object The ActivityPub Object + */ + public function to_object() { + $wp_post = $this->wp_post; + $object = new Base_Object(); + + $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_attributed_to( $this->get_attributed_to() ); + $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; } /** - * Converts this Object into a JSON String + * Returns the User-URL of the Author of the Post. * - * @return string + * If `single_user` mode is enabled, the URL of the Blog-User is returned. + * + * @return string The User-URL. */ - public function to_json() { - return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); + protected function get_attributed_to() { + if ( is_single_user() ) { + $user = new Blog_User(); + return $user->get_url(); + } + + return Users::get_by_id( $this->wp_post->post_author )->get_url(); } /** - * Returns the URL of an Activity Object + * Generates all Image Attachments for a Post. * - * @return string + * @return array The Image Attachments. */ - 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(); @@ -285,7 +180,7 @@ class Post { return $images; } - $id = $this->post->ID; + $id = $this->wp_post->ID; $image_ids = array(); @@ -364,7 +259,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. * @@ -387,64 +282,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': @@ -490,25 +343,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. @@ -524,17 +430,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\"]"; } @@ -549,4 +453,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/templates/admin-header.php b/templates/admin-header.php index 73a830b..3b40468 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -11,6 +11,14 @@ + + + + + + + +
diff --git a/templates/author-json.php b/templates/author-json.php index 7b112ac..70fb43b 100644 --- a/templates/author-json.php +++ b/templates/author-json.php @@ -1,92 +1,14 @@ {'@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' +$user->set_context( + \Activitypub\Activity\Activity::CONTEXT ); -$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 ); /* * 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__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -101,12 +23,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__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__id() ); diff --git a/templates/blog-json.php b/templates/blog-json.php index b87bc94..7ce6a27 100644 --- a/templates/blog-json.php +++ b/templates/blog-json.php @@ -1,66 +1,14 @@ {'@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' +$user->set_context( + \Activitypub\Activity\Activity::CONTEXT ); -$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 ); /* * 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__id() ); $options = 0; // JSON_PRETTY_PRINT added in PHP 5.4 @@ -75,12 +23,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__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__id() ); diff --git a/templates/blog-user-followers-list.php b/templates/blog-user-followers-list.php new file mode 100644 index 0000000..f73826f --- /dev/null +++ b/templates/blog-user-followers-list.php @@ -0,0 +1,30 @@ + '', + 'welcome' => '', + 'followers' => 'active', + ) +); +?> + +
+

+ + + + +

get_user_count() ) ); ?>

+ +
+ + + prepare_items(); + $table->display(); + ?> + +
+
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/templates/settings.php b/templates/settings.php index 0daca17..3da7eaf 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -3,13 +3,14 @@ \dirname( __FILE__ ) . '/admin-header.php', true, array( - 'settings' => 'active', - 'welcome' => '', + 'settings' => 'active', + 'welcome' => '', + 'followers' => '', ) ); ?> -
+

+ + +

+ +

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

+ +

+
+ + + + +

@@ -42,16 +72,44 @@

- - +

- - +

- - +

- - +

@@ -98,13 +156,34 @@

- - +

- - +

- - +

diff --git a/templates/followers-list.php b/templates/user-followers-list.php similarity index 92% rename from templates/followers-list.php rename to templates/user-followers-list.php index 7476192..576b1cf 100644 --- a/templates/followers-list.php +++ b/templates/user-followers-list.php @@ -1,6 +1,5 @@

-

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 @@ diff --git a/templates/welcome.php b/templates/welcome.php index cb4d05f..02e8be3 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' => '', ) ); ?> @@ -13,29 +14,65 @@

+ + + +

%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' diff --git a/tests/test-class-activitypub-activity-dispatcher.php b/tests/test-class-activitypub-activity-dispatcher.php index 70ed304..54ebda0 100644 --- a/tests/test-class-activitypub-activity-dispatcher.php +++ b/tests/test-class-activitypub-activity-dispatcher.php @@ -34,21 +34,25 @@ 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_activity( get_post( $post ), 'Create' ); $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 ); + $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 ); } + public function test_dispatch_mentions() { $post = \wp_insert_post( array( @@ -76,10 +80,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_activity( get_post( $post ), 'Create' ); $this->assertSame( 1, $pre_http_request->get_call_count() ); $all_args = $pre_http_request->get_args(); @@ -93,6 +94,80 @@ 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 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 ); diff --git a/tests/test-class-activitypub-activity.php b/tests/test-class-activitypub-activity.php index 8262f6c..ba9f5a2 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() ); @@ -36,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-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 f0acd34..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(); @@ -42,7 +43,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\Collection\Users::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 +56,8 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase { add_filter( 'pre_get_remote_metadata_by_actor', function( $json, $actor ) { + $user = Activitypub\Collection\Users::get_by_id( 1 ); + $public_key = $user->get__public_key(); // return ActivityPub Profile with signature return array( 'id' => $actor, @@ -60,7 +65,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, ), ); }, @@ -77,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(); diff --git a/tests/test-class-db-activitypub-followers.php b/tests/test-class-db-activitypub-followers.php index 3634713..2f21767 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' ); @@ -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 ) ) {

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

-

+

get_resource() ) ); ?>