update endpoints (#390)
* add collection endpoint * show featured posts * more consistant wording * backwards compatibility with php7.x * compatibility with php5.6 * use ACTIVITYPUB_AUTHORIZED_FETCH instead because the ACTIVITYPUB_SECURE_MODE could be misinterpreted with disabling the security mechanisms completely. * the blog user follows all authors of a blog if not in single_user mode * phpdoc * adding changes based on feedback from @jeherve * global namespace * better hashtag handling should also fix #373 #239 thanks @jeherve for help and feedback! * fix workflow
This commit is contained in:
parent
beb194c395
commit
049046be70
13 changed files with 312 additions and 16 deletions
|
@ -27,7 +27,7 @@ function init() {
|
|||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([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', "<strong>[ap_title]</strong>\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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
|
||||
class Shortcodes {
|
||||
/**
|
||||
* Class constructor, registering WordPress then Shortcodes
|
||||
|
@ -44,9 +46,9 @@ class Shortcodes {
|
|||
|
||||
foreach ( $tags as $tag ) {
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="u-tag u-category" href="%s">#%s</a>',
|
||||
'<a rel="tag" class="u-tag u-category" href="%s">%s</a>',
|
||||
\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(
|
||||
'<a rel="tag" class="u-tag u-category" href="%s">#%s</a>',
|
||||
'<a rel="tag" class="u-tag u-category" href="%s">%s</a>',
|
||||
\esc_url( \get_category_link( $category ) ),
|
||||
\wp_strip_all_tags( $category->slug )
|
||||
esc_hashtag( $category->name )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
162
includes/rest/class-collection.php
Normal file
162
includes/rest/class-collection.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* ActivityPub Collections REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
|
||||
*/
|
||||
class Collection {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( self::class, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/users/(?P<user_id>[\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<user_id>[\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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue