diff --git a/activitypub.php b/activitypub.php
index e826e46..abb68d9 100644
--- a/activitypub.php
+++ b/activitypub.php
@@ -27,7 +27,7 @@ function init() {
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=
)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
- \defined( 'ACTIVITYPUB_SECURE_MODE' ) || \define( 'ACTIVITYPUB_SECURE_MODE', apply_filters( 'activitypub_secure_mode', $value = false ) );
+ \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
@@ -47,6 +47,7 @@ function init() {
Rest\Following::init();
Rest\Webfinger::init();
Rest\Server::init();
+ Rest\Collection::init();
Admin::init();
Hashtag::init();
diff --git a/includes/class-activitypub.php b/includes/class-activitypub.php
index fa2f941..e379c10 100644
--- a/includes/class-activitypub.php
+++ b/includes/class-activitypub.php
@@ -97,7 +97,7 @@ class Activitypub {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
}
- if ( ACTIVITYPUB_SECURE_MODE ) {
+ if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
$verification = Signature::verify_http_signature( $_SERVER );
if ( \is_wp_error( $verification ) ) {
// fallback as template_loader can't return http headers
diff --git a/includes/class-shortcodes.php b/includes/class-shortcodes.php
index 7877b64..d1c71f5 100644
--- a/includes/class-shortcodes.php
+++ b/includes/class-shortcodes.php
@@ -1,6 +1,8 @@
#%s',
+ '%s',
\esc_url( \get_tag_link( $tag ) ),
- \wp_strip_all_tags( $tag->slug )
+ esc_hashtag( $tag->name )
);
}
@@ -357,9 +359,9 @@ class Shortcodes {
foreach ( $categories as $category ) {
$hash_tags[] = \sprintf(
- '#%s',
+ '%s',
\esc_url( \get_category_link( $category ) ),
- \wp_strip_all_tags( $category->slug )
+ esc_hashtag( $category->name )
);
}
diff --git a/includes/collection/class-users.php b/includes/collection/class-users.php
index 036bbc3..f6e35a6 100644
--- a/includes/collection/class-users.php
+++ b/includes/collection/class-users.php
@@ -185,4 +185,25 @@ class Users {
public static function normalize_host( $host ) {
return \str_replace( 'www.', '', $host );
}
+
+ /**
+ * Get the User collection.
+ *
+ * @return array The User collection.
+ */
+ public static function get_collection() {
+ $users = \get_users(
+ array(
+ 'capability__in' => array( 'publish_posts' ),
+ )
+ );
+
+ $return = array();
+
+ foreach ( $users as $user ) {
+ $return[] = User::from_wp_user( $user->ID );
+ }
+
+ return $return;
+ }
}
diff --git a/includes/functions.php b/includes/functions.php
index a3698c8..6d9013d 100644
--- a/includes/functions.php
+++ b/includes/functions.php
@@ -232,6 +232,43 @@ function snake_to_camel_case( $string ) {
return lcfirst( str_replace( '_', '', ucwords( $string, '_' ) ) );
}
+/**
+ * Escapes a Tag, to be used as a hashtag.
+ *
+ * @param string $string The string to escape.
+ *
+ * @return string The escaped hastag.
+ */
+function esc_hashtag( $string ) {
+
+ $hashtag = \wp_specialchars_decode( $string, ENT_QUOTES );
+ // Remove all characters that are not letters, numbers, or underscores.
+ $hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
+
+ // Capitalize every letter that is preceded by an underscore.
+ $hashtag = preg_replace_callback(
+ '/_(.)/',
+ function ( $matches ) {
+ return '' . strtoupper( $matches[1] );
+ },
+ $hashtag
+ );
+
+ // Add a hashtag to the beginning of the string.
+ $hashtag = ltrim( $hashtag, '#' );
+ $hashtag = '#' . $hashtag;
+
+ /**
+ * Allow defining your own custom hashtag generation rules.
+ *
+ * @param string $hashtag The hashtag to be returned.
+ * @param string $string The original string.
+ */
+ $hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $string );
+
+ return esc_html( $hashtag );
+}
+
/**
* Check if a request is for an ActivityPub request.
*
diff --git a/includes/model/class-application-user.php b/includes/model/class-application-user.php
index 888c709..2affde5 100644
--- a/includes/model/class-application-user.php
+++ b/includes/model/class-application-user.php
@@ -98,4 +98,12 @@ class Application_User extends Blog_User {
public function get_attachment() {
return array();
}
+
+ public function get_featured_tags() {
+ return array();
+ }
+
+ public function get_featured() {
+ return array();
+ }
}
diff --git a/includes/model/class-user.php b/includes/model/class-user.php
index 23cd013..63ec86f 100644
--- a/includes/model/class-user.php
+++ b/includes/model/class-user.php
@@ -18,6 +18,24 @@ class User extends Actor {
*/
protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
+ /**
+ * The Featured-Tags.
+ *
+ * @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
+ *
+ * @var string
+ */
+ protected $featured_tags;
+
+ /**
+ * The Featured-Posts.
+ *
+ * @see https://docs.joinmastodon.org/spec/activitypub/#featured
+ *
+ * @var string
+ */
+ protected $featured;
+
/**
* The User-Type
*
@@ -205,6 +223,24 @@ class User extends Actor {
return get_rest_url_by_path( sprintf( 'users/%d/following', $this->get__id() ) );
}
+ /**
+ * Returns the Featured-API-Endpoint.
+ *
+ * @return string The Featured-Endpoint.
+ */
+ public function get_featured() {
+ return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
+ }
+
+ /**
+ * Returns the Featured-Tags-API-Endpoint.
+ *
+ * @return string The Featured-Tags-Endpoint.
+ */
+ public function get_featured_tags() {
+ return get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $this->get__id() ) );
+ }
+
/**
* Extend the User-Output with Attachments.
*
diff --git a/includes/rest/class-collection.php b/includes/rest/class-collection.php
new file mode 100644
index 0000000..00b676d
--- /dev/null
+++ b/includes/rest/class-collection.php
@@ -0,0 +1,162 @@
+[\w\-\.]+)/collections/tags',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( self::class, 'tags_get' ),
+ 'args' => self::request_parameters(),
+ 'permission_callback' => '__return_true',
+ ),
+ )
+ );
+
+ \register_rest_route(
+ ACTIVITYPUB_REST_NAMESPACE,
+ '/users/(?P[\w\-\.]+)/collections/featured',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( self::class, 'featured_get' ),
+ 'args' => self::request_parameters(),
+ 'permission_callback' => '__return_true',
+ ),
+ )
+ );
+ }
+
+ /**
+ * The Featured Tags endpoint
+ *
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return WP_REST_Response The response object.
+ */
+ public static function tags_get( $request ) {
+ $user_id = $request->get_param( 'user_id' );
+ $number = 4;
+
+ $tags = \get_terms(
+ array(
+ 'taxonomy' => 'post_tag',
+ 'orderby' => 'count',
+ 'order' => 'DESC',
+ 'number' => $number,
+ )
+ );
+
+ if ( is_wp_error( $tags ) ) {
+ $tags = array();
+ }
+
+ $response = array(
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ array(
+ 'Hashtah' => 'as:Hastag',
+ ),
+ 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user_id ) ),
+ 'totalItems' => count( $tags ),
+ 'items' => array(),
+ );
+
+ foreach ( $tags as $tag ) {
+ $response['items'][] = array(
+ 'type' => 'Hashtag',
+ 'href' => \esc_url( \get_tag_link( $tag ) ),
+ 'name' => esc_hashtag( $tag->name ),
+ );
+ }
+
+ return new WP_REST_Response( $response, 200 );
+ }
+
+ /**
+ * Featured posts endpoint
+ *
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return WP_REST_Response The response object.
+ */
+ public static function featured_get( $request ) {
+ $user_id = $request->get_param( 'user_id' );
+
+ $args = array(
+ 'post__in' => \get_option( 'sticky_posts' ),
+ 'ignore_sticky_posts' => 1,
+ );
+
+ if ( $user_id > 0 ) {
+ $args['author'] = $user_id;
+ }
+
+ $posts = \get_posts( $args );
+
+ $response = array(
+ '@context' => 'https://www.w3.org/ns/activitystreams',
+ array(
+ 'ostatus' => 'http://ostatus.org#',
+ 'atomUri' => 'ostatus:atomUri',
+ 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri',
+ 'conversation' => 'ostatus:conversation',
+ 'sensitive' => 'as:sensitive',
+ 'toot' => 'http://joinmastodon.org/ns#',
+ 'votersCount' => 'toot:votersCount',
+ ),
+ 'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
+ 'totalItems' => count( $posts ),
+ 'items' => array(),
+ );
+
+ foreach ( $posts as $post ) {
+ $response['items'][] = Post::transform( $post )->to_object()->to_array();
+ }
+
+ return new WP_REST_Response( $response, 200 );
+ }
+
+ /**
+ * The supported parameters
+ *
+ * @return array list of parameters
+ */
+ public static function request_parameters() {
+ $params = array();
+
+ $params['user_id'] = array(
+ 'required' => true,
+ 'type' => 'string',
+ );
+
+ return $params;
+ }
+}
diff --git a/includes/rest/class-following.php b/includes/rest/class-following.php
index f212677..48e3ca0 100644
--- a/includes/rest/class-following.php
+++ b/includes/rest/class-following.php
@@ -3,6 +3,7 @@ namespace Activitypub\Rest;
use Activitypub\Collection\Users as User_Collection;
+use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
/**
@@ -18,6 +19,7 @@ class Following {
*/
public static function init() {
\add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
+ \add_filter( 'activitypub_rest_following', array( self::class, 'default_following' ), 10, 2 );
}
/**
@@ -68,8 +70,11 @@ class Following {
$json->type = 'OrderedCollectionPage';
$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
+
+ $items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore
+
+ $json->totalItems = count( $items ); // phpcs:ignore
+ $json->orderedItems = $items; // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
@@ -98,4 +103,27 @@ class Following {
return $params;
}
+
+ /**
+ * Add the Blog Authors to the following list of the Blog Actor
+ * if Blog not in single mode.
+ *
+ * @param array $array The array of following urls.
+ * @param User $user The user object.
+ *
+ * @return array The array of following urls.
+ */
+ public static function default_following( $array, $user ) {
+ if ( 0 !== $user->get__id() || is_single_user() ) {
+ return $array;
+ }
+
+ $users = User_Collection::get_collection();
+
+ foreach ( $users as $user ) {
+ $array[] = $user->get_url();
+ }
+
+ return $array;
+ }
}
diff --git a/includes/rest/class-inbox.php b/includes/rest/class-inbox.php
index 38969f7..aa8e012 100644
--- a/includes/rest/class-inbox.php
+++ b/includes/rest/class-inbox.php
@@ -102,7 +102,7 @@ class Inbox {
$json->first = $json->partOf; // phpcs:ignore
// filter output
- $json = \apply_filters( 'activitypub_inbox_array', $json );
+ $json = \apply_filters( 'activitypub_rest_inbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
diff --git a/includes/rest/class-outbox.php b/includes/rest/class-outbox.php
index b7ff095..2f50bd7 100644
--- a/includes/rest/class-outbox.php
+++ b/includes/rest/class-outbox.php
@@ -116,7 +116,7 @@ class Outbox {
}
// filter output
- $json = \apply_filters( 'activitypub_outbox_array', $json );
+ $json = \apply_filters( 'activitypub_rest_outbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
diff --git a/includes/rest/class-server.php b/includes/rest/class-server.php
index 3b78af2..325b9ad 100644
--- a/includes/rest/class-server.php
+++ b/includes/rest/class-server.php
@@ -72,9 +72,9 @@ class Server {
// check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
if (
- ! str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
- str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
- str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' )
+ ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
+ \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
+ \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' )
) {
return $response;
}
@@ -86,7 +86,7 @@ class Server {
return $verified_request;
}
} elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode
- if ( ACTIVITYPUB_SECURE_MODE ) {
+ if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
$verified_request = Signature::verify_http_signature( $request );
if ( \is_wp_error( $verified_request ) ) {
return $verified_request;
diff --git a/includes/transformer/class-post.php b/includes/transformer/class-post.php
index e9453dc..cc8a1c6 100644
--- a/includes/transformer/class-post.php
+++ b/includes/transformer/class-post.php
@@ -6,6 +6,7 @@ use Activitypub\Collection\Users;
use Activitypub\Model\Blog_User;
use Activitypub\Activity\Base_Object;
+use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
@@ -379,8 +380,8 @@ class Post {
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 ),
+ 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
+ 'name' => esc_hashtag( $post_tag->name ),
);
$tags[] = $tag;
}