Merge branch 'master' into add/query-follower-list
This commit is contained in:
commit
d2299436a7
24 changed files with 508 additions and 249 deletions
34
README.md
34
README.md
|
@ -2,8 +2,8 @@
|
|||
**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/), [cavalierlife](https://profiles.wordpress.org/cavalierlife/)
|
||||
**Tags:** OStatus, fediverse, activitypub, activitystream
|
||||
**Requires at least:** 4.7
|
||||
**Tested up to:** 6.3
|
||||
**Stable tag:** 1.0.7
|
||||
**Tested up to:** 6.4
|
||||
**Stable tag:** 1.1.0
|
||||
**Requires PHP:** 5.6
|
||||
**License:** MIT
|
||||
**License URI:** http://opensource.org/licenses/MIT
|
||||
|
@ -105,6 +105,36 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
|
|||
|
||||
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
||||
|
||||
### 1.1.0 ###
|
||||
|
||||
* Improved: audio and video attachments are now supported!
|
||||
* Improved: better error messages if remote profile is not accessible
|
||||
* Improved: PHP 8.1 compatibility
|
||||
* Fixed: don't try to parse mentions or hashtags for very large (>1MB) posts to prevent timeouts
|
||||
* Fixed: better handling of ISO-639-1 locale codes
|
||||
* Improved: more reliable [ap_author], props @uk3
|
||||
* Improved: NodeInfo statistics
|
||||
|
||||
### 1.0.10 ###
|
||||
|
||||
* Improved: better error messages if remote profile is not accessible
|
||||
|
||||
### 1.0.9 ###
|
||||
|
||||
* Fixed: broken following endpoint
|
||||
|
||||
### 1.0.8 ###
|
||||
|
||||
* Fixed: blocking of HEAD requests
|
||||
* Fixed: PHP fatal error
|
||||
* Fixed: several typos
|
||||
* Fixed: error codes
|
||||
* Improved: loading of shortcodes
|
||||
* Updated: caching of followers
|
||||
* Updated: Application-User is no longer "indexable"
|
||||
* Updated: more consistent usage of the `application/activity+json` Content-Type
|
||||
* Removed: featured tags endpoint
|
||||
|
||||
### 1.0.7 ###
|
||||
|
||||
* Fixed: broken function call
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: ActivityPub
|
||||
* Plugin URI: https://github.com/pfefferle/wordpress-activitypub/
|
||||
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
|
||||
* Version: 1.0.7
|
||||
* Version: 1.1.0
|
||||
* Author: Matthias Pfefferle & Automattic
|
||||
* Author URI: https://automattic.com/
|
||||
* License: MIT
|
||||
|
@ -69,7 +69,6 @@ function plugin_init() {
|
|||
\add_action( 'init', array( __NAMESPACE__ . '\Collection\Followers', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Shortcodes', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Health_Check', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
|
|
|
@ -43,6 +43,10 @@ class Hashtag {
|
|||
* @return string the filtered post-content
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
// small protection against execution timeouts: limit to 1 MB
|
||||
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
|
||||
return $the_content;
|
||||
}
|
||||
$tag_stack = array();
|
||||
$protected_tags = array(
|
||||
'pre',
|
||||
|
|
|
@ -26,6 +26,10 @@ class Mention {
|
|||
* @return string the filtered post-content
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
// small protection against execution timeouts: limit to 1 MB
|
||||
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
|
||||
return $the_content;
|
||||
}
|
||||
$tag_stack = array();
|
||||
$protected_tags = array(
|
||||
'pre',
|
||||
|
|
|
@ -5,14 +5,9 @@ use function Activitypub\esc_hashtag;
|
|||
|
||||
class Shortcodes {
|
||||
/**
|
||||
* Class constructor, registering WordPress then Shortcodes
|
||||
* Register the shortcodes
|
||||
*/
|
||||
public static function init() {
|
||||
// do not load on admin pages
|
||||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function register() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
if ( 'init' !== $shortcode ) {
|
||||
add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) );
|
||||
|
@ -20,6 +15,17 @@ class Shortcodes {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the shortcodes
|
||||
*/
|
||||
public static function unregister() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
if ( 'init' !== $shortcode ) {
|
||||
remove_shortcode( 'ap_' . $shortcode );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_hashtags' shortcode
|
||||
*
|
||||
|
@ -384,7 +390,8 @@ class Shortcodes {
|
|||
return '';
|
||||
}
|
||||
|
||||
$name = \get_the_author_meta( 'display_name', $item->post_author );
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$name = \get_the_author_meta( 'display_name', $author_id );
|
||||
|
||||
if ( ! $name ) {
|
||||
return '';
|
||||
|
@ -409,7 +416,8 @@ class Shortcodes {
|
|||
return '';
|
||||
}
|
||||
|
||||
$url = \get_the_author_meta( 'user_url', $item->post_author );
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$url = \get_the_author_meta( 'user_url', $author_id );
|
||||
|
||||
if ( ! $url ) {
|
||||
return '';
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace Activitypub;
|
|||
use WP_Error;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
/**
|
||||
|
@ -226,7 +227,7 @@ class Signature {
|
|||
/**
|
||||
* Verifies the http signatures
|
||||
*
|
||||
* @param WP_REQUEST|array $request The request object or $_SERVER array.
|
||||
* @param WP_REST_Request|array $request The request object or $_SERVER array.
|
||||
*
|
||||
* @return mixed A boolean or WP_Error.
|
||||
*/
|
||||
|
@ -259,7 +260,7 @@ class Signature {
|
|||
}
|
||||
|
||||
if ( ! isset( $headers['signature'] ) ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'signature', $headers ) ) {
|
||||
|
@ -269,7 +270,7 @@ class Signature {
|
|||
}
|
||||
|
||||
if ( ! isset( $signature_block ) || ! $signature_block ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$signed_headers = $signature_block['headers'];
|
||||
|
@ -279,12 +280,12 @@ class Signature {
|
|||
|
||||
$signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers );
|
||||
if ( ! $signed_data ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Signed request date outside acceptable time window', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Signed request date outside acceptable time window', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$algorithm = self::get_signature_algorithm( $signature_block );
|
||||
if ( ! $algorithm ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) {
|
||||
|
@ -300,7 +301,7 @@ class Signature {
|
|||
}
|
||||
|
||||
if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid Digest header', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid Digest header', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -313,7 +314,7 @@ class Signature {
|
|||
$verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0;
|
||||
|
||||
if ( ! $verified ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
return $verified;
|
||||
}
|
||||
|
@ -323,17 +324,25 @@ class Signature {
|
|||
*
|
||||
* @param string $key_id The URL to the public key.
|
||||
*
|
||||
* @return WP_Error|string The public key.
|
||||
* @return WP_Error|string The public key or WP_Error.
|
||||
*/
|
||||
public static function get_remote_key( $key_id ) { // phpcs:ignore
|
||||
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); // phpcs:ignore
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_profile_found',
|
||||
__( 'No Profile found or Profile not accessible', 'activitypub' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
|
||||
return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore
|
||||
}
|
||||
return new WP_Error( 'activitypub_no_remote_key_found', __( 'No Public-Key found', 'activitypub' ), array( 'status' => 403 ) );
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_key_found',
|
||||
__( 'No Public-Key found', 'activitypub' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -173,8 +173,6 @@ class Followers {
|
|||
return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$error = null;
|
||||
|
||||
$follower = new Follower();
|
||||
$follower->from_array( $meta );
|
||||
|
||||
|
@ -184,14 +182,10 @@ class Followers {
|
|||
return $id;
|
||||
}
|
||||
|
||||
$meta = get_post_meta( $id, 'activitypub_user_id' );
|
||||
|
||||
if ( $error ) {
|
||||
self::add_error( $id, $error );
|
||||
}
|
||||
$post_meta = get_post_meta( $id, 'activitypub_user_id' );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
if ( is_array( $meta ) && ! in_array( $user_id, $meta ) ) {
|
||||
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
||||
add_post_meta( $id, 'activitypub_user_id', $user_id );
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
|
|
|
@ -35,3 +35,15 @@ if ( ! function_exists( 'get_self_link' ) ) {
|
|||
return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_countable' ) ) {
|
||||
/**
|
||||
* Polyfill for `is_countable()` function added in PHP 7.3.
|
||||
*
|
||||
* @param mixed $value The value to check.
|
||||
* @return bool True if `$value` is countable, otherwise false.
|
||||
*/
|
||||
function is_countable( $value ) {
|
||||
return is_array( $value ) || $value instanceof \Countable;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ use WP_Error;
|
|||
use Activitypub\Http;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
/**
|
||||
* Returns the ActivityPub default JSON-context
|
||||
|
@ -42,7 +43,7 @@ function get_webfinger_resource( $user_id ) {
|
|||
* @param string $actor The Actor URL.
|
||||
* @param bool $cached If the result should be cached.
|
||||
*
|
||||
* @return array The Actor profile as array
|
||||
* @return array|WP_Error The Actor profile as array or WP_Error on failure.
|
||||
*/
|
||||
function get_remote_metadata_by_actor( $actor, $cached = true ) {
|
||||
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
|
||||
|
@ -74,32 +75,25 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
|
|||
|
||||
if ( ! \wp_http_validate_url( $actor ) ) {
|
||||
$metadata = new WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
|
||||
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$short_timeout = function() {
|
||||
return 10;
|
||||
};
|
||||
add_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
||||
$response = Http::get( $actor );
|
||||
remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $response;
|
||||
}
|
||||
|
||||
$metadata = \wp_remote_retrieve_body( $response );
|
||||
$metadata = \json_decode( $metadata, true );
|
||||
|
||||
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
|
||||
|
||||
if ( ! $metadata ) {
|
||||
$metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
|
||||
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
|
@ -481,3 +475,75 @@ function is_json( $data ) {
|
|||
function is_blog_public() {
|
||||
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active users based on a given duration
|
||||
*
|
||||
* @param int $duration The duration to check in month(s)
|
||||
*
|
||||
* @return int The number of active users
|
||||
*/
|
||||
function get_active_users( $duration = 1 ) {
|
||||
|
||||
$duration = intval( $duration );
|
||||
$transient_key = sprintf( 'monthly_active_users_%d', $duration );
|
||||
$count = get_transient( $transient_key );
|
||||
|
||||
if ( false === $count ) {
|
||||
global $wpdb;
|
||||
$query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )";
|
||||
$query = $wpdb->prepare( $query, $duration );
|
||||
$count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
|
||||
set_transient( $transient_key, $count, DAY_IN_SECONDS );
|
||||
}
|
||||
|
||||
// if 0 authors where active
|
||||
if ( 0 === $count ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// if single user mode
|
||||
if ( is_single_user() ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// if blog user is disabled
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
return $count;
|
||||
}
|
||||
|
||||
// also count blog user
|
||||
return $count + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of users
|
||||
*
|
||||
* @return int The total number of users
|
||||
*/
|
||||
function get_total_users() {
|
||||
// if single user mode
|
||||
if ( is_single_user() ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_array( $users ) ) {
|
||||
$users = count( $users );
|
||||
} else {
|
||||
$users = 1;
|
||||
}
|
||||
|
||||
// if blog user is disabled
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
return $users;
|
||||
}
|
||||
|
||||
return $users + 1;
|
||||
}
|
||||
|
|
|
@ -58,10 +58,6 @@ class Application_User extends Blog_User {
|
|||
return null;
|
||||
}
|
||||
|
||||
public function get_featured_tags() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_featured() {
|
||||
return null;
|
||||
}
|
||||
|
@ -69,4 +65,8 @@ class Application_User extends Blog_User {
|
|||
public function get_moderators() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_indexable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,15 +18,6 @@ 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.
|
||||
*
|
||||
|
@ -235,15 +226,6 @@ class User extends Actor {
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -105,7 +105,7 @@ class Collection {
|
|||
'@context' => Activity::CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/tags', $user->get__id() ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => count( $tags ),
|
||||
'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
|
@ -117,7 +117,10 @@ class Collection {
|
|||
);
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $response, 200 );
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -160,7 +163,7 @@ class Collection {
|
|||
'@context' => Activity::CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $user_id ) ),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => count( $posts ),
|
||||
'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
|
@ -168,7 +171,10 @@ class Collection {
|
|||
$response['orderedItems'][] = Post::transform( $post )->to_object()->to_array();
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $response, 200 );
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -192,7 +198,10 @@ class Collection {
|
|||
$response['orderedItems'][] = $user->get_url();
|
||||
}
|
||||
|
||||
return new WP_REST_Response( $response, 200 );
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,10 +103,10 @@ class Followers {
|
|||
$data['followers']
|
||||
);
|
||||
|
||||
$response = new WP_REST_Response( $json, 200 );
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
|
@ -74,15 +75,15 @@ class Following {
|
|||
|
||||
$items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore
|
||||
|
||||
$json->totalItems = count( $items ); // phpcs:ignore
|
||||
$json->totalItems = is_countable( $items ) ? count( $items ) : 0; // phpcs:ignore
|
||||
$json->orderedItems = $items; // phpcs:ignore
|
||||
|
||||
$json->first = $json->partOf; // phpcs:ignore
|
||||
|
||||
$response = new \WP_REST_Response( $json, 200 );
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -109,11 +109,10 @@ class Inbox {
|
|||
*/
|
||||
\do_action( 'activitypub_inbox_post' );
|
||||
|
||||
$response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -138,7 +137,10 @@ class Inbox {
|
|||
\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 );
|
||||
$rest_response = new WP_REST_Response( array(), 202 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -158,7 +160,7 @@ class Inbox {
|
|||
'rest_invalid_param',
|
||||
\__( 'No recipients found', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'status' => 400,
|
||||
'params' => array(
|
||||
'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
|
||||
'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
|
||||
|
@ -183,7 +185,10 @@ class Inbox {
|
|||
\do_action( "activitypub_inbox_{$type}", $data, $user->ID );
|
||||
}
|
||||
|
||||
return new WP_REST_Response( array(), 202 );
|
||||
$rest_response = new WP_REST_Response( array(), 202 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -416,7 +421,7 @@ class Inbox {
|
|||
$recipient_items = array_merge( $recipient_items, $recipient );
|
||||
}
|
||||
|
||||
if ( array_key_exists( $i, $data['object'] ) ) {
|
||||
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
|
||||
if ( is_array( $data['object'][ $i ] ) ) {
|
||||
$recipient = $data['object'][ $i ];
|
||||
} else {
|
||||
|
|
|
@ -3,6 +3,8 @@ namespace Activitypub\Rest;
|
|||
|
||||
use WP_REST_Response;
|
||||
|
||||
use function Activitypub\get_total_users;
|
||||
use function Activitypub\get_active_users;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
|
@ -82,24 +84,14 @@ class Nodeinfo {
|
|||
'version' => \get_bloginfo( 'version' ),
|
||||
);
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_array( $users ) ) {
|
||||
$users = count( $users );
|
||||
} else {
|
||||
$users = 1;
|
||||
}
|
||||
|
||||
$posts = \wp_count_posts();
|
||||
$comments = \wp_count_comments();
|
||||
|
||||
$nodeinfo['usage'] = array(
|
||||
'users' => array(
|
||||
'total' => $users,
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
|
@ -139,24 +131,14 @@ class Nodeinfo {
|
|||
'version' => \get_bloginfo( 'version' ),
|
||||
);
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_array( $users ) ) {
|
||||
$users = count( $users );
|
||||
} else {
|
||||
$users = 1;
|
||||
}
|
||||
|
||||
$posts = \wp_count_posts();
|
||||
$comments = \wp_count_comments();
|
||||
|
||||
$nodeinfo['usage'] = array(
|
||||
'users' => array(
|
||||
'total' => (int) $users,
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( 1 ),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
|
|
|
@ -123,11 +123,10 @@ class Outbox {
|
|||
*/
|
||||
\do_action( 'activitypub_outbox_post' );
|
||||
|
||||
$response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
namespace Activitypub\Rest;
|
||||
|
||||
use stdClass;
|
||||
use WP_Error;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Model\Application_User;
|
||||
|
@ -54,11 +55,10 @@ class Server {
|
|||
|
||||
$json = $user->to_array();
|
||||
|
||||
$response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,6 +74,10 @@ class Server {
|
|||
* @return mixed|WP_Error The response, error, or modified response.
|
||||
*/
|
||||
public static function authorize_activitypub_requests( $response, $handler, $request ) {
|
||||
if ( 'HEAD' === $request->get_method() ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$route = $request->get_route();
|
||||
|
||||
// check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
|
||||
|
@ -86,16 +90,16 @@ class Server {
|
|||
}
|
||||
|
||||
// POST-Requets are always signed
|
||||
if ( 'get' !== \strtolower( $request->get_method() ) ) {
|
||||
if ( 'GET' !== $request->get_method() ) {
|
||||
$verified_request = Signature::verify_http_signature( $request );
|
||||
if ( \is_wp_error( $verified_request ) ) {
|
||||
return $verified_request;
|
||||
return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
|
||||
}
|
||||
} elseif ( 'get' === \strtolower( $request->get_method() ) ) { // GET-Requests are only signed in secure mode
|
||||
} elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode
|
||||
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
$verified_request = Signature::verify_http_signature( $request );
|
||||
if ( \is_wp_error( $verified_request ) ) {
|
||||
return $verified_request;
|
||||
return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -95,10 +95,10 @@ class Users {
|
|||
|
||||
$json = $user->to_array();
|
||||
|
||||
$response = new WP_REST_Response( $json, 200 );
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use WP_Post;
|
|||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Model\Blog_User;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Shortcodes;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\is_single_user;
|
||||
|
@ -81,7 +82,7 @@ class Post {
|
|||
$object->set_content( $this->get_content() );
|
||||
$object->set_content_map(
|
||||
array(
|
||||
\strstr( \get_locale(), '_', true ) => $this->get_content(),
|
||||
$this->get_locale() => $this->get_content(),
|
||||
)
|
||||
);
|
||||
$path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
|
||||
|
@ -142,78 +143,65 @@ class Post {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the Image Attachments for this Post, parsed from blocks.
|
||||
* @param int $max_images The maximum number of images to return.
|
||||
* @param array $image_ids The image IDs to append new IDs to.
|
||||
* Generates all Media Attachments for a Post.
|
||||
*
|
||||
* @return array The image IDs.
|
||||
*/
|
||||
protected function get_block_image_ids( $max_images, $image_ids = [] ) {
|
||||
$blocks = \parse_blocks( $this->wp_post->post_content );
|
||||
return self::get_image_ids_from_blocks( $blocks, $image_ids, $max_images );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively get image IDs from blocks.
|
||||
* @param array $blocks The blocks to search for image IDs
|
||||
* @param array $image_ids The image IDs to append new IDs to
|
||||
* @param int $max_images The maximum number of images to return.
|
||||
*
|
||||
* @return array The image IDs.
|
||||
*/
|
||||
protected static function get_image_ids_from_blocks( $blocks, $image_ids, $max_images ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
// recurse into inner blocks
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$image_ids = self::get_image_ids_from_blocks( $block['innerBlocks'], $image_ids, $max_images );
|
||||
}
|
||||
|
||||
switch ( $block['blockName'] ) {
|
||||
case 'core/image':
|
||||
case 'core/cover':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$image_ids[] = $block['attrs']['id'];
|
||||
}
|
||||
break;
|
||||
case 'jetpack/slideshow':
|
||||
case 'jetpack/tiled-gallery':
|
||||
if ( ! empty( $block['attrs']['ids'] ) ) {
|
||||
$image_ids = array_merge( $image_ids, $block['attrs']['ids'] );
|
||||
}
|
||||
break;
|
||||
case 'jetpack/image-compare':
|
||||
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
|
||||
$image_ids[] = $block['attrs']['beforeImageId'];
|
||||
}
|
||||
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
|
||||
$image_ids[] = $block['attrs']['afterImageId'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// we could be at or over max, stop unneeded work
|
||||
if ( count( $image_ids ) >= $max_images ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// still need to slice it because one gallery could knock us over the limit
|
||||
return \array_slice( $image_ids, 0, $max_images );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates all Image Attachments for a Post.
|
||||
*
|
||||
* @return array The Image Attachments.
|
||||
* @return array The Attachments.
|
||||
*/
|
||||
protected function get_attachments() {
|
||||
$max_images = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
|
||||
// Once upon a time we only supported images, but we now support audio/video as well.
|
||||
// We maintain the image-centric naming for backwards compatibility.
|
||||
$max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
|
||||
|
||||
$images = array();
|
||||
if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
|
||||
return $this->get_block_attachments( $max_media );
|
||||
}
|
||||
|
||||
return $this->get_classic_editor_images( $max_media );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
|
||||
*
|
||||
* @param int $max_media The maximum number of attachments to return.
|
||||
*
|
||||
* @return array The attachments.
|
||||
*/
|
||||
protected function get_block_attachments( $max_media ) {
|
||||
// max media can't be negative or zero
|
||||
if ( $max_media <= 0 ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$id = $this->wp_post->ID;
|
||||
|
||||
$media_ids = array();
|
||||
|
||||
// list post thumbnail first if this post has one
|
||||
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
||||
$media_ids[] = \get_post_thumbnail_id( $id );
|
||||
}
|
||||
|
||||
if ( $max_media > 0 ) {
|
||||
$blocks = \parse_blocks( $this->wp_post->post_content );
|
||||
$media_ids = self::get_media_ids_from_blocks( $blocks, $media_ids, $max_media );
|
||||
}
|
||||
$media_ids = \array_unique( $media_ids );
|
||||
|
||||
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media_ids ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image attachments from the classic editor.
|
||||
* Note that audio/video attachments are only supported in the block editor.
|
||||
*
|
||||
* @param int $max_images The maximum number of images to return.
|
||||
*
|
||||
* @return array The attachments.
|
||||
*/
|
||||
protected function get_classic_editor_images( $max_images ) {
|
||||
// max images can't be negative or zero
|
||||
if ( $max_images <= 0 ) {
|
||||
return $images;
|
||||
return array();
|
||||
}
|
||||
|
||||
$id = $this->wp_post->ID;
|
||||
|
@ -227,68 +215,144 @@ class Post {
|
|||
}
|
||||
|
||||
if ( $max_images > 0 ) {
|
||||
// first try to get images that are actually in the post content
|
||||
if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
|
||||
$block_image_ids = $this->get_block_image_ids( $max_images, $image_ids );
|
||||
$image_ids = \array_merge( $image_ids, $block_image_ids );
|
||||
} else {
|
||||
// fallback to images attached to the post
|
||||
$query = new \WP_Query(
|
||||
array(
|
||||
'post_parent' => $id,
|
||||
'post_status' => 'inherit',
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'image',
|
||||
'order' => 'ASC',
|
||||
'orderby' => 'menu_order ID',
|
||||
'posts_per_page' => $max_images,
|
||||
)
|
||||
);
|
||||
foreach ( $query->get_posts() as $attachment ) {
|
||||
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
|
||||
$image_ids[] = $attachment->ID;
|
||||
}
|
||||
$query = new \WP_Query(
|
||||
array(
|
||||
'post_parent' => $id,
|
||||
'post_status' => 'inherit',
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'image',
|
||||
'order' => 'ASC',
|
||||
'orderby' => 'menu_order ID',
|
||||
'posts_per_page' => $max_images,
|
||||
)
|
||||
);
|
||||
foreach ( $query->get_posts() as $attachment ) {
|
||||
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
|
||||
$image_ids[] = $attachment->ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$image_ids = \array_unique( $image_ids );
|
||||
|
||||
// get URLs for each image
|
||||
foreach ( $image_ids as $id ) {
|
||||
$image_size = 'full';
|
||||
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the image URL returned for each post.
|
||||
*
|
||||
* @param array|false $thumbnail The image URL, or false if no image is available.
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||
*/
|
||||
$thumbnail = apply_filters(
|
||||
'activitypub_get_image',
|
||||
$this->get_image( $id, $image_size ),
|
||||
$id,
|
||||
$image_size
|
||||
);
|
||||
/**
|
||||
* Recursively get media IDs from blocks.
|
||||
* @param array $blocks The blocks to search for media IDs
|
||||
* @param array $media_ids The media IDs to append new IDs to
|
||||
* @param int $max_media The maximum number of media to return.
|
||||
*
|
||||
* @return array The image IDs.
|
||||
*/
|
||||
protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
|
||||
|
||||
if ( $thumbnail ) {
|
||||
$mimetype = \get_post_mime_type( $id );
|
||||
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => $thumbnail[0],
|
||||
'mediaType' => $mimetype,
|
||||
);
|
||||
foreach ( $blocks as $block ) {
|
||||
// recurse into inner blocks
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
|
||||
}
|
||||
|
||||
if ( $alt ) {
|
||||
$image['name'] = $alt;
|
||||
}
|
||||
$images[] = $image;
|
||||
switch ( $block['blockName'] ) {
|
||||
case 'core/image':
|
||||
case 'core/cover':
|
||||
case 'core/audio':
|
||||
case 'core/video':
|
||||
case 'videopress/video':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$media_ids[] = $block['attrs']['id'];
|
||||
}
|
||||
break;
|
||||
case 'jetpack/slideshow':
|
||||
case 'jetpack/tiled-gallery':
|
||||
if ( ! empty( $block['attrs']['ids'] ) ) {
|
||||
$media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
|
||||
}
|
||||
break;
|
||||
case 'jetpack/image-compare':
|
||||
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
|
||||
$media_ids[] = $block['attrs']['beforeImageId'];
|
||||
}
|
||||
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
|
||||
$media_ids[] = $block['attrs']['afterImageId'];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// stop doing unneeded work
|
||||
if ( count( $media_ids ) >= $max_media ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $images;
|
||||
// still need to slice it because one gallery could knock us over the limit
|
||||
return array_slice( $media_ids, 0, $max_media );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WordPress Attachment to an ActivityPub Attachment.
|
||||
*
|
||||
* @param int $id The Attachment ID.
|
||||
*
|
||||
* @return array The ActivityPub Attachment.
|
||||
*/
|
||||
public static function wp_attachment_to_activity_attachment( $id ) {
|
||||
$attachment = array();
|
||||
$mime_type = \get_post_mime_type( $id );
|
||||
$mime_type_parts = \explode( '/', $mime_type );
|
||||
// switching on image/audio/video
|
||||
switch ( $mime_type_parts[0] ) {
|
||||
case 'image':
|
||||
$image_size = 'full';
|
||||
|
||||
/**
|
||||
* Filter the image URL returned for each post.
|
||||
*
|
||||
* @param array|false $thumbnail The image URL, or false if no image is available.
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||
*/
|
||||
$thumbnail = apply_filters(
|
||||
'activitypub_get_image',
|
||||
self::get_image( $id, $image_size ),
|
||||
$id,
|
||||
$image_size
|
||||
);
|
||||
|
||||
if ( $thumbnail ) {
|
||||
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => $thumbnail[0],
|
||||
'mediaType' => $mime_type,
|
||||
);
|
||||
|
||||
if ( $alt ) {
|
||||
$image['name'] = $alt;
|
||||
}
|
||||
$attachment = $image;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
case 'video':
|
||||
$attachment = array(
|
||||
'type' => 'Document',
|
||||
'mediaType' => $mime_type,
|
||||
'url' => \wp_get_attachment_url( $id ),
|
||||
'name' => \get_the_title( $id ),
|
||||
);
|
||||
$meta = wp_get_attachment_metadata( $id );
|
||||
// height and width for videos
|
||||
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
|
||||
$attachment['width'] = $meta['width'];
|
||||
$attachment['height'] = $meta['height'];
|
||||
}
|
||||
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
|
||||
break;
|
||||
}
|
||||
|
||||
return \apply_filters( 'activitypub_attachment', $attachment, $id );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -299,7 +363,7 @@ class Post {
|
|||
*
|
||||
* @return array|false Array of image data, or boolean false if no image is available.
|
||||
*/
|
||||
protected function get_image( $id, $image_size = 'full' ) {
|
||||
protected static function get_image( $id, $image_size = 'full' ) {
|
||||
/**
|
||||
* Hook into the image retrieval process. Before image retrieval.
|
||||
*
|
||||
|
@ -308,7 +372,7 @@ class Post {
|
|||
*/
|
||||
do_action( 'activitypub_get_image_pre', $id, $image_size );
|
||||
|
||||
$thumbnail = \wp_get_attachment_image_src( $id, $image_size );
|
||||
$image = \wp_get_attachment_image_src( $id, $image_size );
|
||||
|
||||
/**
|
||||
* Hook into the image retrieval process. After image retrieval.
|
||||
|
@ -318,7 +382,7 @@ class Post {
|
|||
*/
|
||||
do_action( 'activitypub_get_image_post', $id, $image_size );
|
||||
|
||||
return $thumbnail;
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -334,6 +398,8 @@ class Post {
|
|||
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
|
||||
}
|
||||
|
||||
// Default to Article.
|
||||
$object_type = 'Article';
|
||||
$post_type = \get_post_type( $this->wp_post );
|
||||
switch ( $post_type ) {
|
||||
case 'post':
|
||||
|
@ -466,6 +532,8 @@ class Post {
|
|||
$post = $this->wp_post;
|
||||
$content = $this->get_post_content_template();
|
||||
|
||||
// Register our shortcodes just in time.
|
||||
Shortcodes::register();
|
||||
// Fill in the shortcodes.
|
||||
setup_postdata( $post );
|
||||
$content = do_shortcode( $content );
|
||||
|
@ -477,6 +545,9 @@ class Post {
|
|||
|
||||
$content = \apply_filters( 'activitypub_the_content', $content, $post );
|
||||
|
||||
// Don't need these any more, should never appear in a post.
|
||||
Shortcodes::unregister();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
|
@ -509,4 +580,25 @@ class Post {
|
|||
protected function get_mentions() {
|
||||
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale of the post.
|
||||
*
|
||||
* @return string The locale of the post.
|
||||
*/
|
||||
public function get_locale() {
|
||||
$post_id = $this->wp_post->ID;
|
||||
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
||||
|
||||
/**
|
||||
* Filter the locale of the post.
|
||||
*
|
||||
* @param string $lang The locale of the post.
|
||||
* @param int $post_id The post ID.
|
||||
* @param WP_Post $post The post object.
|
||||
*
|
||||
* @return string The filtered locale of the post.
|
||||
*/
|
||||
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\get_total_users;
|
||||
use function Activitypub\get_active_users;
|
||||
|
||||
/**
|
||||
* Compatibility with the NodeInfo plugin
|
||||
*
|
||||
|
@ -31,6 +34,12 @@ class Nodeinfo {
|
|||
$nodeinfo['protocols']['outbound'][] = 'activitypub';
|
||||
}
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
|
@ -44,6 +53,12 @@ class Nodeinfo {
|
|||
public static function add_nodeinfo2_discovery( $nodeinfo ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
}
|
||||
|
|
34
readme.txt
34
readme.txt
|
@ -2,8 +2,8 @@
|
|||
Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife
|
||||
Tags: OStatus, fediverse, activitypub, activitystream
|
||||
Requires at least: 4.7
|
||||
Tested up to: 6.3
|
||||
Stable tag: 1.0.7
|
||||
Tested up to: 6.4
|
||||
Stable tag: 1.1.0
|
||||
Requires PHP: 5.6
|
||||
License: MIT
|
||||
License URI: http://opensource.org/licenses/MIT
|
||||
|
@ -105,6 +105,36 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
|
|||
|
||||
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
||||
|
||||
= 1.1.0 =
|
||||
|
||||
* Improved: audio and video attachments are now supported!
|
||||
* Improved: better error messages if remote profile is not accessible
|
||||
* Improved: PHP 8.1 compatibility
|
||||
* Fixed: don't try to parse mentions or hashtags for very large (>1MB) posts to prevent timeouts
|
||||
* Fixed: better handling of ISO-639-1 locale codes
|
||||
* Improved: more reliable [ap_author], props @uk3
|
||||
* Improved: NodeInfo statistics
|
||||
|
||||
= 1.0.10 =
|
||||
|
||||
* Improved: better error messages if remote profile is not accessible
|
||||
|
||||
= 1.0.9 =
|
||||
|
||||
* Fixed: broken following endpoint
|
||||
|
||||
= 1.0.8 =
|
||||
|
||||
* Fixed: blocking of HEAD requests
|
||||
* Fixed: PHP fatal error
|
||||
* Fixed: several typos
|
||||
* Fixed: error codes
|
||||
* Improved: loading of shortcodes
|
||||
* Updated: caching of followers
|
||||
* Updated: Application-User is no longer "indexable"
|
||||
* Updated: more consistent usage of the `application/activity+json` Content-Type
|
||||
* Removed: featured tags endpoint
|
||||
|
||||
= 1.0.7 =
|
||||
|
||||
* Fixed: broken function call
|
||||
|
|
|
@ -138,7 +138,7 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Number of images', 'activitypub' ); ?>
|
||||
<?php \esc_html_e( 'Media attachments', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<input value="<?php echo esc_attr( \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ); ?>" name="activitypub_max_image_attachments" id="activitypub_max_image_attachments" type="number" min="0" />
|
||||
|
@ -147,13 +147,20 @@
|
|||
echo \wp_kses(
|
||||
\sprintf(
|
||||
// translators:
|
||||
\__( 'The number of images to attach to posts. Default: <code>%s</code>', 'activitypub' ),
|
||||
\__( 'The number of media (images, audio, video) to attach to posts. Default: <code>%s</code>', 'activitypub' ),
|
||||
\esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
|
||||
),
|
||||
'default'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<em>
|
||||
<?php
|
||||
esc_html_e( 'Note: audio and video attachments are only supported from Block Editor.', 'activitypub' );
|
||||
?>
|
||||
</em>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?php
|
||||
use Activitypub\Shortcodes;
|
||||
|
||||
class Test_Activitypub_Shortcodes extends WP_UnitTestCase {
|
||||
|
||||
public function test_content() {
|
||||
Shortcodes::register();
|
||||
global $post;
|
||||
|
||||
$post_id = -99; // negative ID, to avoid clash with a valid post
|
||||
|
@ -26,9 +30,11 @@ class Test_Activitypub_Shortcodes extends WP_UnitTestCase {
|
|||
wp_reset_postdata();
|
||||
|
||||
$this->assertEquals( '<p>hallo</p>', $content );
|
||||
Shortcodes::unregister();
|
||||
}
|
||||
|
||||
public function test_password_protected_content() {
|
||||
Shortcodes::register();
|
||||
global $post;
|
||||
|
||||
$post_id = -98; // negative ID, to avoid clash with a valid post
|
||||
|
@ -54,5 +60,6 @@ class Test_Activitypub_Shortcodes extends WP_UnitTestCase {
|
|||
wp_reset_postdata();
|
||||
|
||||
$this->assertEquals( '', $content );
|
||||
Shortcodes::unregister();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue