Merge branch 'master' into fix/sanitization

This commit is contained in:
Matthias Pfefferle 2023-05-23 19:20:23 +02:00 committed by GitHub
commit 27dd8217e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 826 additions and 213 deletions

View file

@ -20,11 +20,12 @@ The plugin works with the following tested federated platforms, but there may be
* [Mastodon](https://joinmastodon.org/) * [Mastodon](https://joinmastodon.org/)
* [Pleroma](https://pleroma.social/) * [Pleroma](https://pleroma.social/)
* [Friendica](https://friendi.ca/) * [friendica](https://friendi.ca/)
* [HubZilla](https://hubzilla.org/) * [Hubzilla](https://hubzilla.org/)
* [Pixelfed](https://pixelfed.org/) * [Pixelfed](https://pixelfed.org/)
* [SocialHome](https://socialhome.network/) * [Socialhome](https://socialhome.network/)
* [Misskey](https://join.misskey.page/) * [Misskey](https://join.misskey.page/)
* [Calckey](https://calckey.org/)
Heres what that means and what you can expect. Heres what that means and what you can expect.
@ -94,7 +95,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U
Add the following to the .htaccess file in the root directory: Add the following to the .htaccess file in the root directory:
RedirectMatch "^\/\.well-known(.*)$" "\/blog\/\.well-known$1" RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1$2"
Where 'blog' is the path to the subdirectory at which your blog resides. Where 'blog' is the path to the subdirectory at which your blog resides.
@ -115,6 +116,8 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
### Next ### ### Next ###
* Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests.
* Compatibility: add hooks to allow modifying images returned in ActivityPub requests.
* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2.
### 0.17.0 ### ### 0.17.0 ###

View file

@ -15,6 +15,8 @@
namespace Activitypub; namespace Activitypub;
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
/** /**
* Initialize plugin * Initialize plugin
*/ */
@ -30,76 +32,72 @@ function init() {
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
\define( 'ACTIVITYPUB_OBJECT', 'ACTIVITYPUB_OBJECT' );
require_once \dirname( __FILE__ ) . '/includes/table/class-followers.php';
require_once \dirname( __FILE__ ) . '/includes/class-http.php';
require_once \dirname( __FILE__ ) . '/includes/class-signature.php';
require_once \dirname( __FILE__ ) . '/includes/class-webfinger.php';
require_once \dirname( __FILE__ ) . '/includes/peer/class-followers.php';
require_once \dirname( __FILE__ ) . '/includes/functions.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-activity.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-post.php';
require_once \dirname( __FILE__ ) . '/includes/model/class-follower.php';
require_once \dirname( __FILE__ ) . '/includes/class-migration.php';
Migration::init(); Migration::init();
require_once \dirname( __FILE__ ) . '/includes/class-activity-dispatcher.php';
Activity_Dispatcher::init();
require_once \dirname( __FILE__ ) . '/includes/class-activitypub.php';
Activitypub::init(); Activitypub::init();
Activity_Dispatcher::init();
require_once \dirname( __FILE__ ) . '/includes/collection/class-followers.php';
Collection\Followers::init(); Collection\Followers::init();
// Configure the REST API route // Configure the REST API route
require_once \dirname( __FILE__ ) . '/includes/rest/class-outbox.php';
Rest\Outbox::init(); Rest\Outbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-inbox.php';
Rest\Inbox::init(); Rest\Inbox::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-followers.php';
Rest\Followers::init(); Rest\Followers::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-following.php';
Rest\Following::init(); Rest\Following::init();
require_once \dirname( __FILE__ ) . '/includes/rest/class-webfinger.php';
Rest\Webfinger::init(); Rest\Webfinger::init();
// load NodeInfo endpoints only if blog is public
if ( true === (bool) \get_option( 'blog_public', 1 ) ) {
require_once \dirname( __FILE__ ) . '/includes/rest/class-nodeinfo.php';
Rest\NodeInfo::init();
}
require_once \dirname( __FILE__ ) . '/includes/class-admin.php';
Admin::init(); Admin::init();
require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php';
Hashtag::init(); Hashtag::init();
require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php';
Shortcodes::init(); Shortcodes::init();
require_once \dirname( __FILE__ ) . '/includes/class-mention.php';
Mention::init(); Mention::init();
require_once \dirname( __FILE__ ) . '/includes/class-debug.php';
Debug::init();
require_once \dirname( __FILE__ ) . '/includes/class-health-check.php';
Health_Check::init(); Health_Check::init();
Scheduler::init();
if ( \WP_DEBUG ) { }
require_once \dirname( __FILE__ ) . '/includes/debug.php'; \add_action( 'plugins_loaded', __NAMESPACE__ . '\init' );
}
/**
* Class Autoloader
*/
spl_autoload_register(
function ( $full_class ) {
$base_dir = \dirname( __FILE__ ) . '/includes/';
$base = 'activitypub';
$class = strtolower( $full_class );
if ( strncmp( $class, $base, strlen( $base ) ) === 0 ) {
$class = str_replace( 'activitypub\\', '', $class );
if ( false !== strpos( $class, '\\' ) ) {
$parts = explode( '\\', $class );
$class = array_pop( $parts );
$sub_dir = implode( '/', $parts );
$base_dir = $base_dir . $sub_dir . '/';
}
$filename = 'class-' . strtr( $class, '_', '-' );
$file = $base_dir . $filename . '.php';
if ( file_exists( $file ) && is_readable( $file ) ) {
require_once $file;
} else {
// translators: %s is the class name
\wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) );
}
}
}
);
require_once \dirname( __FILE__ ) . '/includes/functions.php';
// load NodeInfo endpoints only if blog is public
if ( \get_option( 'blog_public', 1 ) ) {
Rest\NodeInfo::init();
}
$debug_file = \dirname( __FILE__ ) . '/includes/debug.php';
if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) {
require_once $debug_file;
Debug::init();
} }
\add_action( 'plugins_loaded', '\Activitypub\init' );
/** /**
* Add plugin settings link * Add plugin settings link
@ -113,43 +111,54 @@ function plugin_settings_link( $actions ) {
return \array_merge( $settings_link, $actions ); return \array_merge( $settings_link, $actions );
} }
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), '\Activitypub\plugin_settings_link' ); \add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' );
/** \register_activation_hook(
* Add rewrite rules __FILE__,
*/ array(
function add_rewrite_rules() { __NAMESPACE__ . '\Activitypub',
if ( ! \class_exists( 'Webfinger' ) ) { 'activate',
\add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' ); )
} );
if ( ! \class_exists( 'Nodeinfo' ) || ! (bool) \get_option( 'blog_public', 1 ) ) { \register_deactivation_hook(
\add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); __FILE__,
\add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); array(
} __NAMESPACE__ . '\Activitypub',
'deactivate',
)
);
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); \register_uninstall_hook(
} __FILE__,
\add_action( 'init', '\Activitypub\add_rewrite_rules', 1 ); array(
__NAMESPACE__ . '\Activitypub',
/** 'uninstall',
* Flush rewrite rules; )
*/ );
function flush_rewrite_rules() {
\Activitypub\add_rewrite_rules();
\flush_rewrite_rules();
}
\register_activation_hook( __FILE__, '\Activitypub\flush_rewrite_rules' );
\register_deactivation_hook( __FILE__, '\flush_rewrite_rules' );
/** /**
* Only load code that needs BuddyPress to run once BP is loaded and initialized. * Only load code that needs BuddyPress to run once BP is loaded and initialized.
*/ */
function enable_buddypress_features() { add_action(
require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php'; 'bp_include',
Integration\Buddypress::init(); function() {
} require_once \dirname( __FILE__ ) . '/integration/class-buddypress.php';
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); Integration\Buddypress::init();
},
0
);
add_action(
'plugins_loaded',
function() {
if ( defined( 'WP_SWEEP_VERSION' ) ) {
require_once \dirname( __FILE__ ) . '/integration/class-wp-sweep.php';
Integration\Wp_Sweep::init();
}
},
0
);
/** /**
* `get_plugin_data` wrapper * `get_plugin_data` wrapper

View file

@ -13,7 +13,7 @@
"squizlabs/php_codesniffer": "3.*", "squizlabs/php_codesniffer": "3.*",
"wp-coding-standards/wpcs": "*", "wp-coding-standards/wpcs": "*",
"yoast/phpunit-polyfills": "^1.0", "yoast/phpunit-polyfills": "^1.0",
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.1" "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0"
}, },
"config": { "config": {
"allow-plugins": true "allow-plugins": true

View file

@ -22,9 +22,40 @@ class Activitypub {
\add_post_type_support( $post_type, 'activitypub' ); \add_post_type_support( $post_type, 'activitypub' );
} }
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
\add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 ); \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
\add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
\add_action( 'init', array( self::class, 'add_rewrite_rules' ) );
}
/**
* Activation Hook
*
* @return void
*/
public static function activate() {
self::flush_rewrite_rules();
Scheduler::register_schedules();
}
/**
* Deactivation Hook
*
* @return void
*/
public static function deactivate() {
self::flush_rewrite_rules();
Scheduler::deregister_schedules();
}
/**
* Uninstall Hook
*
* @return void
*/
public static function uninstall() {
} }
/** /**
@ -98,36 +129,6 @@ class Activitypub {
return $vars; return $vars;
} }
/**
* Schedule Activities.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
*/
public static function schedule_post_activity( $new_status, $old_status, $post ) {
// Do not send activities if post is password protected.
if ( \post_password_required( $post ) ) {
return;
}
// Check if post-type supports ActivityPub.
$post_types = \get_post_types_by_support( 'activitypub' );
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
return;
}
$activitypub_post = new \Activitypub\Model\Post( $post );
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
\wp_schedule_single_event( \time(), 'activitypub_send_create_activity', array( $activitypub_post ) );
} elseif ( 'publish' === $new_status ) {
\wp_schedule_single_event( \time(), 'activitypub_send_update_activity', array( $activitypub_post ) );
} elseif ( 'trash' === $new_status ) {
\wp_schedule_single_event( \time(), 'activitypub_send_delete_activity', array( $activitypub_post ) );
}
}
/** /**
* Replaces the default avatar. * Replaces the default avatar.
* *
@ -146,7 +147,14 @@ class Activitypub {
} }
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
if ( ! empty( $id_or_email->comment_type ) && ! \in_array( $id_or_email->comment_type, (array) $allowed_comment_types, true ) ) { if (
! empty( $id_or_email->comment_type ) &&
! \in_array(
$id_or_email->comment_type,
(array) $allowed_comment_types,
true
)
) {
$args['url'] = false; $args['url'] = false;
/** This filter is documented in wp-includes/link-template.php */ /** This filter is documented in wp-includes/link-template.php */
return \apply_filters( 'get_avatar_data', $args, $id_or_email ); return \apply_filters( 'get_avatar_data', $args, $id_or_email );
@ -191,7 +199,12 @@ class Activitypub {
* @return void * @return void
*/ */
public static function trash_post( $post_id ) { public static function trash_post( $post_id ) {
\add_post_meta( $post_id, 'activitypub_canonical_url', \get_permalink( $post_id ), true ); \add_post_meta(
$post_id,
'activitypub_canonical_url',
\get_permalink( $post_id ),
true
);
} }
/** /**
@ -204,4 +217,40 @@ class Activitypub {
public static function untrash_post( $post_id ) { public static function untrash_post( $post_id ) {
\delete_post_meta( $post_id, 'activitypub_canonical_url' ); \delete_post_meta( $post_id, 'activitypub_canonical_url' );
} }
/**
* Add rewrite rules
*/
public static function add_rewrite_rules() {
if ( ! \class_exists( 'Webfinger' ) ) {
\add_rewrite_rule(
'^.well-known/webfinger',
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
'top'
);
}
if ( ! \class_exists( 'Nodeinfo' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
\add_rewrite_rule(
'^.well-known/nodeinfo',
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
'top'
);
\add_rewrite_rule(
'^.well-known/x-nodeinfo2',
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
'top'
);
}
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
}
/**
* Flush rewrite rules;
*/
public static function flush_rewrite_rules() {
self::add_rewrite_rules();
\flush_rewrite_rules();
}
} }

View file

@ -1,9 +1,18 @@
<?php <?php
namespace Activitypub; namespace Activitypub;
use Acctivitypub\Model\Follower; use Activitypub\Model\Follower;
use Activitypub\Collection\Followers;
/**
* ActivityPub Migration Class
*
* @author Matthias Pfefferle
*/
class Migration { class Migration {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() { public static function init() {
\add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) ); \add_action( 'activitypub_schedule_migration', array( self::class, 'maybe_migrate' ) );
} }
@ -74,7 +83,7 @@ class Migration {
$follower->upsert(); $follower->upsert();
$result = wp_set_object_terms( $user_id, $follower->get_actor(), self::TAXONOMY, true ); $result = wp_set_object_terms( $user_id, $follower->get_actor(), Followers::TAXONOMY, true );
} }
} }
} }

View file

@ -0,0 +1,138 @@
<?php
namespace Activitypub;
use Activitypub\Model\Post;
use Activitypub\Collection\Followers;
/**
* ActivityPub Scheduler Class
*
* @author Matthias Pfefferle
*/
class Scheduler {
/**
* Initialize the class, registering WordPress hooks
*/
public static function init() {
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
}
/**
* Schedule all ActivityPub schedules.
*
* @return void
*/
public static function register_schedules() {
if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
\wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
}
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
}
}
/**
* Unscedule all ActivityPub schedules.
*
* @return void
*/
public static function deregister_schedules() {
wp_unschedule_hook( 'activitypub_update_followers' );
wp_unschedule_hook( 'activitypub_cleanup_followers' );
}
/**
* Schedule Activities.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
*/
public static function schedule_post_activity( $new_status, $old_status, $post ) {
// Do not send activities if post is password protected.
if ( \post_password_required( $post ) ) {
return;
}
// Check if post-type supports ActivityPub.
$post_types = \get_post_types_by_support( 'activitypub' );
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
return;
}
$activitypub_post = new Post( $post );
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
\wp_schedule_single_event(
\time(),
'activitypub_send_create_activity',
array( $activitypub_post )
);
} elseif ( 'publish' === $new_status ) {
\wp_schedule_single_event(
\time(),
'activitypub_send_update_activity',
array( $activitypub_post )
);
} elseif ( 'trash' === $new_status ) {
\wp_schedule_single_event(
\time(),
'activitypub_send_delete_activity',
array( $activitypub_post )
);
}
}
/**
* Update followers
*
* @return void
*/
public static function update_followers() {
$followers = Followers::get_outdated_followers();
foreach ( $followers as $follower ) {
$meta = get_remote_metadata_by_actor( $follower->get_actor() );
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
$follower->set_error( $meta );
} else {
$follower->from_meta( $meta );
}
$follower->update();
}
}
/**
* Cleanup followers
*
* @return void
*/
public static function cleanup_followers() {
$followers = Followers::get_faulty_followers();
foreach ( $followers as $follower ) {
$meta = get_remote_metadata_by_actor( $follower->get_actor() );
if ( is_tombstone( $meta ) ) {
$follower->delete();
} elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
if ( 5 <= $follower->count_errors() ) {
$follower->delete();
} else {
$follower->set_error( $meta );
$follower->update();
}
} else {
$follower->reset_errors();
}
}
}
}

View file

@ -6,9 +6,9 @@ class Shortcodes {
* Class constructor, registering WordPress then Shortcodes * Class constructor, registering WordPress then Shortcodes
*/ */
public static function init() { public static function init() {
foreach ( get_class_methods( 'Activitypub\Shortcodes' ) as $shortcode ) { foreach ( get_class_methods( self::class ) as $shortcode ) {
if ( 'init' !== $shortcode ) { if ( 'init' !== $shortcode ) {
add_shortcode( 'ap_' . $shortcode, array( 'Activitypub\Shortcodes', $shortcode ) ); add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) );
} }
} }
} }

View file

@ -19,6 +19,7 @@ use function Activitypub\get_remote_metadata_by_actor;
*/ */
class Followers { class Followers {
const TAXONOMY = 'activitypub-followers'; const TAXONOMY = 'activitypub-followers';
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
/** /**
* Register WordPress hooks/actions and register Taxonomy * Register WordPress hooks/actions and register Taxonomy
@ -143,7 +144,7 @@ class Followers {
'single' => true, 'single' => true,
'sanitize_callback' => function( $value ) { 'sanitize_callback' => function( $value ) {
if ( ! is_numeric( $value ) && (int) $value !== $value ) { if ( ! is_numeric( $value ) && (int) $value !== $value ) {
$value = strtotime( 'now' ); $value = \time();
} }
return $value; return $value;
@ -186,7 +187,7 @@ class Followers {
} }
/** /**
* Handles "Unfollow" requests * Handle "Unfollow" requests
* *
* @param array $object The JSON "Undo" Activity * @param array $object The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User * @param int $user_id The ID of the ID of the WordPress User
@ -202,7 +203,7 @@ class Followers {
} }
/** /**
* Add a new Follower * Add new Follower
* *
* @param int $user_id The ID of the WordPress User * @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL * @param string $actor The Actor URL
@ -225,6 +226,7 @@ class Followers {
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
return $result; return $result;
} else { } else {
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
return $follower; return $follower;
} }
} }
@ -238,6 +240,7 @@ class Followers {
* @return bool|WP_Error True on success, false or WP_Error on failure. * @return bool|WP_Error True on success, false or WP_Error on failure.
*/ */
public static function remove_follower( $user_id, $actor ) { public static function remove_follower( $user_id, $actor ) {
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY ); return wp_remove_object_terms( $user_id, $actor, self::TAXONOMY );
} }
@ -316,7 +319,7 @@ class Followers {
* *
* @return array The Term list of Followers, the format depends on $output * @return array The Term list of Followers, the format depends on $output
*/ */
public static function get_followers( $user_id, $output = ARRAY_N, $number = null, $offset = null, $args = array() ) { public static function get_followers( $user_id, $number = null, $offset = null, $args = array() ) {
$defaults = array( $defaults = array(
'taxonomy' => self::TAXONOMY, 'taxonomy' => self::TAXONOMY,
'hide_empty' => false, 'hide_empty' => false,
@ -329,25 +332,32 @@ class Followers {
$args = wp_parse_args( $args, $defaults ); $args = wp_parse_args( $args, $defaults );
$terms = new WP_Term_Query( $args ); $terms = new WP_Term_Query( $args );
$items = array(); $items = array();
// change output format foreach ( $terms->get_terms() as $follower ) {
switch ( $output ) { $items[] = new Follower( $follower->name ); // phpcs:ignore
case ACTIVITYPUB_OBJECT:
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
case OBJECT:
return $terms->get_terms();
case ARRAY_N:
default:
foreach ( $terms->get_terms() as $follower ) {
$items[] = $follower->name; // phpcs:ignore
}
return $items;
} }
return $items;
}
/**
* Get all Followers
*
* @param array $args The WP_Term_Query arguments.
*
* @return array The Term list of Followers.
*/
public static function get_all_followers( $args = array() ) {
$defaults = array(
'taxonomy' => self::TAXONOMY,
'hide_empty' => false,
);
$args = wp_parse_args( $args, $defaults );
$terms = new WP_Term_Query( $args );
return $terms->get_terms();
} }
/** /**
@ -369,6 +379,13 @@ class Followers {
* @return array The list of Inboxes * @return array The list of Inboxes
*/ */
public static function get_inboxes( $user_id ) { public static function get_inboxes( $user_id ) {
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
$inboxes = wp_cache_get( $cache_key, 'activitypub' );
if ( $inboxes ) {
return $inboxes;
}
// get all Followers of a ID of the WordPress User // get all Followers of a ID of the WordPress User
$terms = new WP_Term_Query( $terms = new WP_Term_Query(
array( array(
@ -402,6 +419,75 @@ class Followers {
) )
); );
return array_filter( $results ); $inboxes = array_filter( $results );
wp_cache_set( $cache_key, $inboxes, 'activitypub' );
return $inboxes;
}
/**
* Get all Followers that have not been updated for a given time
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT.
* @param int $number Limits the result.
* @param int $older_than The time in seconds.
*
* @return mixed The Term list of Followers, the format depends on $output.
*/
public static function get_outdated_followers( $number = 50, $older_than = 604800 ) {
$args = array(
'taxonomy' => self::TAXONOMY,
'number' => $number,
'meta_key' => 'updated_at',
'orderby' => 'meta_value_num',
'order' => 'DESC',
'meta_query' => array(
array(
'key' => 'updated_at',
'value' => time() - $older_than,
'type' => 'numeric',
'compare' => '<=',
),
),
);
$terms = new WP_Term_Query( $args );
$items = array();
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
}
/**
* Get all Followers that had errors
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
* @param integer $number The number of Followers to return.
*
* @return mixed The Term list of Followers, the format depends on $output.
*/
public static function get_faulty_followers( $number = 10 ) {
$args = array(
'taxonomy' => self::TAXONOMY,
'number' => $number,
'meta_query' => array(
array(
'key' => 'errors',
'compare' => 'EXISTS',
),
),
);
$terms = new WP_Term_Query( $args );
$items = array();
foreach ( $terms->get_terms() as $follower ) {
$items[] = new Follower( $follower->name ); // phpcs:ignore
}
return $items;
} }
} }

View file

@ -205,7 +205,7 @@ function get_author_description( $user_id ) {
if ( empty( $description ) ) { if ( empty( $description ) ) {
$description = get_user_meta( $user_id, 'description', true ); $description = get_user_meta( $user_id, 'description', true );
} }
return $description; return \wpautop( \wp_kses( $description, 'default' ) );
} }
/** /**
@ -228,3 +228,61 @@ function is_tombstone( $wp_error ) {
return false; return false;
} }
/**
* Get the REST URL relative to this plugin's namespace.
*
* @param string $path Optional. REST route path. Otherwise this plugin's namespaced root.
*
* @return string REST URL relative to this plugin's namespace.
*/
function get_rest_url_by_path( $path = '' ) {
// we'll handle the leading slash.
$path = ltrim( $path, '/' );
$namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
return \get_rest_url( null, $namespaced_path );
}
/**
* Check if a request is for an ActivityPub request.
*
* @return bool False by default.
*/
function is_activitypub_request() {
global $wp_query;
/*
* ActivityPub requests are currently only made for
* author archives, singular posts, and the homepage.
*/
if ( ! \is_author() && ! \is_singular() && ! \is_home() ) {
return false;
}
// One can trigger an ActivityPub request by adding ?activitypub to the URL.
global $wp_query;
if ( isset( $wp_query->query_vars['activitypub'] ) ) {
return true;
}
/*
* The other (more common) option to make an ActivityPub request
* is to send an Accept header.
*/
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
$accept = $_SERVER['HTTP_ACCEPT'];
/*
* $accept can be a single value, or a comma separated list of values.
* We want to support both scenarios,
* and return true when the header includes at least one of the following:
* - application/activity+json
* - application/ld+json
*/
if ( preg_match( '/(application\/(ld\+json|activity\+json))/', $accept ) ) {
return true;
}
}
return false;
}

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub\Model; namespace Activitypub\Model;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub Post Class * ActivityPub Post Class
* *
@ -97,7 +99,7 @@ class Activity {
} }
$this->type = \ucfirst( $type ); $this->type = \ucfirst( $type );
$this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) ); $this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \time() );
} }
/** /**
@ -148,7 +150,8 @@ class Activity {
$this->published = $object['published']; $this->published = $object['published'];
} }
$this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) ); $path = sprintf( 'users/%d/followers', intval( $post->get_post_author() ) );
$this->add_to( get_rest_url_by_path( $path ) );
if ( isset( $this->object['attributedTo'] ) ) { if ( isset( $this->object['attributedTo'] ) ) {
$this->actor = $this->object['attributedTo']; $this->actor = $this->object['attributedTo'];

View file

@ -139,8 +139,8 @@ class Follower {
/** /**
* Magic function to implement getter and setter * Magic function to implement getter and setter
* *
* @param string $method * @param string $method The method name.
* @param string $params * @param string $params The method params.
* *
* @return void * @return void
*/ */
@ -159,6 +159,22 @@ class Follower {
} }
} }
/**
* Magic function to return the Actor-URL when the Object is used as a string
*
* @return string
*/
public function __toString() {
return $this->get_actor();
}
/**
* Prefill the Object with the meta data.
*
* @param array $meta The meta data.
*
* @return void
*/
public function from_meta( $meta ) { public function from_meta( $meta ) {
$this->meta = $meta; $this->meta = $meta;
@ -178,9 +194,16 @@ class Follower {
$this->shared_inbox = $meta['inbox']; $this->shared_inbox = $meta['inbox'];
} }
$this->updated_at = \strtotime( 'now' ); $this->updated_at = \time();
} }
/**
* Get the data by the given attribute
*
* @param string $attribute The attribute name.
*
* @return mixed The attribute value.
*/
public function get( $attribute ) { public function get( $attribute ) {
if ( $this->$attribute ) { if ( $this->$attribute ) {
return $this->$attribute; return $this->$attribute;
@ -201,6 +224,23 @@ class Follower {
return null; return null;
} }
/**
* 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() { public function get_errors() {
if ( $this->errors ) { if ( $this->errors ) {
return $this->errors; return $this->errors;
@ -210,6 +250,20 @@ class Follower {
return $this->errors; return $this->errors;
} }
/**
* Reset (delete) all errors.
*
* @return void
*/
public function reset_errors() {
delete_term_meta( $this->id, 'errors' );
}
/**
* Count the errors.
*
* @return int The number of errors.
*/
public function count_errors() { public function count_errors() {
$errors = $this->get_errors(); $errors = $this->get_errors();
@ -220,6 +274,11 @@ class Follower {
return 0; return 0;
} }
/**
* Return the latest error message.
*
* @return string The error message.
*/
public function get_latest_error_message() { public function get_latest_error_message() {
$errors = $this->get_errors(); $errors = $this->get_errors();
@ -230,6 +289,13 @@ class Follower {
return ''; return '';
} }
/**
* Get the meta data by the given attribute.
*
* @param string $attribute The attribute name.
*
* @return mixed $attribute The attribute value.
*/
public function get_meta_by( $attribute ) { public function get_meta_by( $attribute ) {
$meta = $this->get_meta(); $meta = $this->get_meta();
@ -248,6 +314,11 @@ class Follower {
return null; return null;
} }
/**
* Get the meta data.
*
* @return array $meta The meta data.
*/
public function get_meta() { public function get_meta() {
if ( $this->meta ) { if ( $this->meta ) {
return $this->meta; return $this->meta;
@ -256,6 +327,11 @@ class Follower {
return null; return null;
} }
/**
* Update the current Follower-Object.
*
* @return void
*/
public function update() { public function update() {
$term = wp_update_term( $term = wp_update_term(
$this->id, $this->id,
@ -265,10 +341,15 @@ class Follower {
) )
); );
$this->updated_at = \strtotime( 'now' ); $this->updated_at = \time();
$this->update_term_meta(); $this->update_term_meta();
} }
/**
* Save the current Follower-Object.
*
* @return void
*/
public function save() { public function save() {
$term = wp_insert_term( $term = wp_insert_term(
$this->actor, $this->actor,
@ -284,6 +365,11 @@ class Follower {
$this->update_term_meta(); $this->update_term_meta();
} }
/**
* Upsert the current Follower-Object.
*
* @return void
*/
public function upsert() { public function upsert() {
if ( $this->id ) { if ( $this->id ) {
$this->update(); $this->update();
@ -292,6 +378,20 @@ class Follower {
} }
} }
/**
* Delete the current Follower-Object.
*
* @return void
*/
public function delete() {
wp_delete_term( $this->id, Followers::TAXONOMY );
}
/**
* Update the term meta.
*
* @return void
*/
protected function update_term_meta() { protected function update_term_meta() {
$attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' ); $attributes = array( 'inbox', 'shared_inbox', 'avatar', 'updated_at', 'name', 'username' );
@ -312,6 +412,5 @@ class Follower {
add_term_meta( $this->id, 'errors', $error ); add_term_meta( $this->id, 'errors', $error );
} }
} }
} }

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub\Model; namespace Activitypub\Model;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub Post Class * ActivityPub Post Class
* *
@ -93,8 +95,13 @@ class Post {
'class' => array(), 'class' => array(),
), ),
'ul' => array(), 'ul' => array(),
'ol' => array(), 'ol' => array(
'li' => array(), 'reversed' => array(),
'start' => array(),
),
'li' => array(
'value' => array(),
),
'strong' => array( 'strong' => array(
'class' => array(), 'class' => array(),
), ),
@ -142,7 +149,8 @@ class Post {
*/ */
public function __construct( $post ) { public function __construct( $post ) {
$this->post = \get_post( $post ); $this->post = \get_post( $post );
$this->add_to( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $this->get_post_author() ) . '/followers' ) ); $path = sprintf( 'users/%d/followers', intval( $this->get_post_author() ) );
$this->add_to( get_rest_url_by_path( $path ) );
} }
/** /**
@ -539,6 +547,6 @@ class Post {
return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]"; return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]";
} }
return \get_option( 'activitypub_custom_post_content' ); return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
} }
} }

View file

@ -7,6 +7,8 @@ use WP_REST_Server;
use WP_REST_Response; use WP_REST_Response;
use Activitypub\Collection\Followers as FollowerCollection; use Activitypub\Collection\Followers as FollowerCollection;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub Followers REST-Class * ActivityPub Followers REST-Class
* *
@ -27,7 +29,7 @@ class Followers {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>\d+)/followers', '/users/(?P<user_id>\d+)/followers',
array( array(
array( array(
@ -78,10 +80,16 @@ class Followers {
$json->actor = \get_author_posts_url( $user_id ); $json->actor = \get_author_posts_url( $user_id );
$json->type = 'OrderedCollectionPage'; $json->type = 'OrderedCollectionPage';
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/followers', $user_id ) ); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore $json->first = $json->partOf; // phpcs:ignore
$json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore $json->totalItems = FollowerCollection::count_followers( $user_id ); // phpcs:ignore
$json->orderedItems = FollowerCollection::get_followers( $user_id, ARRAY_N ); // phpcs:ignore // phpcs:ignore
$json->orderedItems = array_map(
function( $item ) {
return $item->get_actor();
},
FollowerCollection::get_followers( $user_id )
);
$response = new WP_REST_Response( $json, 200 ); $response = new WP_REST_Response( $json, 200 );
$response->header( 'Content-Type', 'application/activity+json' ); $response->header( 'Content-Type', 'application/activity+json' );

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub Following REST-Class * ActivityPub Following REST-Class
* *
@ -21,7 +23,7 @@ class Following {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>\d+)/following', '/users/(?P<user_id>\d+)/following',
array( array(
array( array(
@ -72,7 +74,7 @@ class Following {
$json->actor = \get_author_posts_url( $user_id ); $json->actor = \get_author_posts_url( $user_id );
$json->type = 'OrderedCollectionPage'; $json->type = 'OrderedCollectionPage';
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/following" ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/following', $user_id ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore $json->totalItems = 0; // phpcs:ignore
$json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore $json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore

View file

@ -1,8 +1,16 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Model\Activity; use Activitypub\Model\Activity;
use function Activitypub\get_context;
use function Activitypub\url_to_authorid;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_remote_metadata_by_actor;
/** /**
* ActivityPub Inbox REST-Class * ActivityPub Inbox REST-Class
* *
@ -26,11 +34,11 @@ class Inbox {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/inbox', '/inbox',
array( array(
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
'callback' => array( self::class, 'shared_inbox_post' ), 'callback' => array( self::class, 'shared_inbox_post' ),
'args' => self::shared_inbox_post_parameters(), 'args' => self::shared_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
@ -39,17 +47,17 @@ class Inbox {
); );
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>\d+)/inbox', '/users/(?P<user_id>\d+)/inbox',
array( array(
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
'callback' => array( self::class, 'user_inbox_post' ), 'callback' => array( self::class, 'user_inbox_post' ),
'args' => self::user_inbox_post_parameters(), 'args' => self::user_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'user_inbox_get' ), 'callback' => array( self::class, 'user_inbox_get' ),
'args' => self::user_inbox_get_parameters(), 'args' => self::user_inbox_get_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
@ -104,11 +112,11 @@ class Inbox {
$json = new \stdClass(); $json = new \stdClass();
$json->{'@context'} = \Activitypub\get_context(); $json->{'@context'} = get_context();
$json->id = \home_url( \add_query_arg( null, null ) ); $json->id = \home_url( \add_query_arg( null, null ) );
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
$json->type = 'OrderedCollectionPage'; $json->type = 'OrderedCollectionPage';
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/inbox" ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user_id ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore $json->totalItems = 0; // phpcs:ignore
@ -124,7 +132,7 @@ class Inbox {
*/ */
\do_action( 'activitypub_inbox_post' ); \do_action( 'activitypub_inbox_post' );
$response = new \WP_REST_Response( $json, 200 ); $response = new WP_REST_Response( $json, 200 );
$response->header( 'Content-Type', 'application/activity+json' ); $response->header( 'Content-Type', 'application/activity+json' );
@ -148,7 +156,7 @@ class Inbox {
\do_action( 'activitypub_inbox', $data, $user_id, $type ); \do_action( 'activitypub_inbox', $data, $user_id, $type );
\do_action( "activitypub_inbox_{$type}", $data, $user_id ); \do_action( "activitypub_inbox_{$type}", $data, $user_id );
return new \WP_REST_Response( array(), 202 ); return new WP_REST_Response( array(), 202 );
} }
/** /**
@ -164,7 +172,7 @@ class Inbox {
$users = self::extract_recipients( $data ); $users = self::extract_recipients( $data );
if ( ! $users ) { if ( ! $users ) {
return new \WP_Error( return new WP_Error(
'rest_invalid_param', 'rest_invalid_param',
\__( 'No recipients found', 'activitypub' ), \__( 'No recipients found', 'activitypub' ),
array( array(
@ -187,7 +195,7 @@ class Inbox {
\do_action( "activitypub_inbox_{$type}", $data, $user->ID ); \do_action( "activitypub_inbox_{$type}", $data, $user->ID );
} }
return new \WP_REST_Response( array(), 202 ); return new WP_REST_Response( array(), 202 );
} }
/** /**
@ -348,7 +356,7 @@ class Inbox {
* @param int $user_id The id of the local blog-user * @param int $user_id The id of the local blog-user
*/ */
public static function handle_reaction( $object, $user_id ) { public static function handle_reaction( $object, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); $meta = get_remote_metadata_by_actor( $object['actor'] );
$comment_post_id = \url_to_postid( $object['object'] ); $comment_post_id = \url_to_postid( $object['object'] );
@ -393,7 +401,7 @@ class Inbox {
* @param int $user_id The id of the local blog-user * @param int $user_id The id of the local blog-user
*/ */
public static function handle_create( $object, $user_id ) { public static function handle_create( $object, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); $meta = get_remote_metadata_by_actor( $object['actor'] );
if ( ! isset( $object['object']['inReplyTo'] ) ) { if ( ! isset( $object['object']['inReplyTo'] ) ) {
return; return;
@ -500,7 +508,7 @@ class Inbox {
$users = array(); $users = array();
foreach ( $recipients as $recipient ) { foreach ( $recipients as $recipient ) {
$user_id = \Activitypub\url_to_authorid( $recipient ); $user_id = url_to_authorid( $recipient );
$user = get_user_by( 'id', $user_id ); $user = get_user_by( 'id', $user_id );

View file

@ -1,6 +1,8 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub NodeInfo REST-Class * ActivityPub NodeInfo REST-Class
* *
@ -23,7 +25,7 @@ class Nodeinfo {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/nodeinfo/discovery', '/nodeinfo/discovery',
array( array(
array( array(
@ -35,7 +37,7 @@ class Nodeinfo {
); );
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/nodeinfo', '/nodeinfo',
array( array(
array( array(
@ -47,7 +49,7 @@ class Nodeinfo {
); );
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/nodeinfo2', '/nodeinfo2',
array( array(
array( array(
@ -173,7 +175,7 @@ class Nodeinfo {
$discovery['links'] = array( $discovery['links'] = array(
array( array(
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
'href' => \get_rest_url( null, 'activitypub/1.0/nodeinfo' ), 'href' => get_rest_url_by_path( 'nodeinfo' ),
), ),
); );

View file

@ -14,7 +14,7 @@ class Ostatus {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/ostatus/remote-follow', '/ostatus/remote-follow',
array( array(
array( array(

View file

@ -1,6 +1,16 @@
<?php <?php
namespace Activitypub\Rest; namespace Activitypub\Rest;
use stdClass;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Model\Post;
use Activitypub\Model\Activity;
use function Activitypub\get_context;
use function Activitypub\get_rest_url_by_path;
/** /**
* ActivityPub Outbox REST-Class * ActivityPub Outbox REST-Class
* *
@ -21,11 +31,11 @@ class Outbox {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/users/(?P<user_id>\d+)/outbox', '/users/(?P<user_id>\d+)/outbox',
array( array(
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'user_outbox_get' ), 'callback' => array( self::class, 'user_outbox_get' ),
'args' => self::request_parameters(), 'args' => self::request_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
@ -46,7 +56,7 @@ class Outbox {
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ); $post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
if ( ! $author ) { if ( ! $author ) {
return new \WP_Error( return new WP_Error(
'rest_invalid_param', 'rest_invalid_param',
\__( 'User not found', 'activitypub' ), \__( 'User not found', 'activitypub' ),
array( array(
@ -65,14 +75,14 @@ class Outbox {
*/ */
\do_action( 'activitypub_outbox_pre' ); \do_action( 'activitypub_outbox_pre' );
$json = new \stdClass(); $json = new stdClass();
$json->{'@context'} = \Activitypub\get_context(); $json->{'@context'} = get_context();
$json->id = \home_url( \add_query_arg( null, null ) ); $json->id = \home_url( \add_query_arg( null, null ) );
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' ); $json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
$json->actor = \get_author_posts_url( $user_id ); $json->actor = \get_author_posts_url( $user_id );
$json->type = 'OrderedCollectionPage'; $json->type = 'OrderedCollectionPage';
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/outbox" ); // phpcs:ignore $json->partOf = get_rest_url_by_path( sprintf( 'users/%d/outbox', $user_id ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore $json->totalItems = 0; // phpcs:ignore
// phpcs:ignore // phpcs:ignore
@ -101,8 +111,8 @@ class Outbox {
); );
foreach ( $posts as $post ) { foreach ( $posts as $post ) {
$activitypub_post = new \Activitypub\Model\Post( $post ); $activitypub_post = new Post( $post );
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', false ); $activitypub_activity = new Activity( 'Create', false );
$activitypub_activity->from_post( $activitypub_post ); $activitypub_activity->from_post( $activitypub_post );
$json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore $json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore
@ -117,7 +127,7 @@ class Outbox {
*/ */
\do_action( 'activitypub_outbox_post' ); \do_action( 'activitypub_outbox_post' );
$response = new \WP_REST_Response( $json, 200 ); $response = new WP_REST_Response( $json, 200 );
$response->header( 'Content-Type', 'application/activity+json' ); $response->header( 'Content-Type', 'application/activity+json' );

View file

@ -25,7 +25,7 @@ class Webfinger {
*/ */
public static function register_routes() { public static function register_routes() {
\register_rest_route( \register_rest_route(
'activitypub/1.0', ACTIVITYPUB_REST_NAMESPACE,
'/webfinger', '/webfinger',
array( array(
array( array(

View file

@ -35,7 +35,7 @@ class Followers extends WP_List_Table {
$page_num = $this->get_pagenum(); $page_num = $this->get_pagenum();
$per_page = 20; $per_page = 20;
$follower = FollowerCollection::get_followers( \get_current_user_id(), ACTIVITYPUB_OBJECT, $per_page, ( $page_num - 1 ) * $per_page ); $follower = FollowerCollection::get_followers( \get_current_user_id(), $per_page, ( $page_num - 1 ) * $per_page );
$counter = FollowerCollection::count_followers( \get_current_user_id() ); $counter = FollowerCollection::count_followers( \get_current_user_id() );
$this->items = array(); $this->items = array();

View file

@ -3,7 +3,7 @@ namespace Activitypub\Integration;
class Buddypress { class Buddypress {
public static function init() { public static function init() {
\add_filter( 'activitypub_json_author_array', array( 'Activitypub\Integration\Buddypress', 'add_user_metadata' ), 11, 2 ); \add_filter( 'activitypub_json_author_array', array( self::class, 'add_user_metadata' ), 11, 2 );
} }
public static function add_user_metadata( $object, $author_id ) { public static function add_user_metadata( $object, $author_id ) {

View file

@ -0,0 +1,50 @@
<?php
namespace Activitypub\Integration;
use Activitypub\Collection\Followers;
/**
* Manages the compatibility with WP Sweep.
*
* @link https://wordpress.org/plugins/wp-sweep/
* @link https://github.com/polylang/polylang/tree/master/integrations/wp-sweep
*/
class Wp_Sweep {
/**
* Setups actions.
*
* @return void
*/
public static function init() {
add_filter( 'wp_sweep_excluded_taxonomies', array( self::class, 'excluded_taxonomies' ) );
add_filter( 'wp_sweep_excluded_termids', array( self::class, 'excluded_termids' ), 0 );
}
/**
* Add 'activitypub-followers' to excluded taxonomies otherwise terms loose their language
* and translation group.
*
* @param array $excluded_taxonomies List of taxonomies excluded from sweeping.
*
* @return array The list of taxonomies excluded from sweeping.
*/
public static function excluded_taxonomies( $excluded_taxonomies ) {
return array_merge( $excluded_taxonomies, array( Followers::TAXONOMY ) );
}
/**
* Add the translation of the default taxonomy terms and our language terms to the excluded terms.
*
* @param array $excluded_term_ids List of term ids excluded from sweeping.
*
* @return array The list of term ids excluded from sweeping.
*/
public static function excluded_termids( $excluded_term_ids ) {
// We got a list of excluded terms (defaults and parents). Let exclude their translations too.
$followers = Followers::get_all_followers( array( 'fields' => 'ids' ) );
$excluded_term_ids = array_merge( $excluded_term_ids, $followers );
return array_unique( $excluded_term_ids );
}
}

View file

@ -20,11 +20,12 @@ The plugin works with the following tested federated platforms, but there may be
* [Mastodon](https://joinmastodon.org/) * [Mastodon](https://joinmastodon.org/)
* [Pleroma](https://pleroma.social/) * [Pleroma](https://pleroma.social/)
* [Friendica](https://friendi.ca/) * [friendica](https://friendi.ca/)
* [HubZilla](https://hubzilla.org/) * [Hubzilla](https://hubzilla.org/)
* [Pixelfed](https://pixelfed.org/) * [Pixelfed](https://pixelfed.org/)
* [SocialHome](https://socialhome.network/) * [Socialhome](https://socialhome.network/)
* [Misskey](https://join.misskey.page/) * [Misskey](https://join.misskey.page/)
* [Calckey](https://calckey.org/)
Heres what that means and what you can expect. Heres what that means and what you can expect.
@ -94,7 +95,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U
Add the following to the .htaccess file in the root directory: Add the following to the .htaccess file in the root directory:
RedirectMatch "^\/\.well-known(.*)$" "\/blog\/\.well-known$1" RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" "\/blog\/\.well-known$1$2"
Where 'blog' is the path to the subdirectory at which your blog resides. Where 'blog' is the path to the subdirectory at which your blog resides.
@ -115,6 +116,7 @@ Project maintained on GitHub at [automattic/wordpress-activitypub](https://githu
= Next = = Next =
* Compatibility: add a new conditional, `\Activitypub\is_activitypub_request()`, to allow third-party plugins to detect ActivityPub requests.
* Compatibility: add hooks to allow modifying images returned in ActivityPub requests. * Compatibility: add hooks to allow modifying images returned in ActivityPub requests.
* Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2. * Compatibility: indicate that the plugin is compatible and has been tested with the latest version of WordPress, 6.2.

View file

@ -28,10 +28,10 @@ if ( \has_header_image() ) {
); );
} }
$json->inbox = \get_rest_url( null, "/activitypub/1.0/users/$author_id/inbox" ); $json->inbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/inbox', $author_id ) );
$json->outbox = \get_rest_url( null, "/activitypub/1.0/users/$author_id/outbox" ); $json->outbox = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/outbox', $author_id ) );
$json->followers = \get_rest_url( null, "/activitypub/1.0/users/$author_id/followers" ); $json->followers = \Activitypub\get_rest_url_by_path( sprintf( 'users/%d/followers', $author_id ) );
$json->following = \get_rest_url( null, "/activitypub/1.0/users/$author_id/following" ); $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 $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore

View file

@ -27,10 +27,10 @@ if ( \has_header_image() ) {
); );
} }
$json->inbox = \get_rest_url( null, '/activitypub/1.0/blog/inbox' ); $json->inbox = \Activitypub\get_rest_url_by_path( 'blog/inbox' );
$json->outbox = \get_rest_url( null, '/activitypub/1.0/blog/outbox' ); $json->outbox = \Activitypub\get_rest_url_by_path( 'blog/outbox' );
$json->followers = \get_rest_url( null, '/activitypub/1.0/blog/followers' ); $json->followers = \Activitypub\get_rest_url_by_path( 'blog/followers' );
$json->following = \get_rest_url( null, '/activitypub/1.0/blog/following' ); $json->following = \Activitypub\get_rest_url_by_path( 'blog/following' );
$json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_approves_followers', \__return_false() ); // phpcs:ignore

View file

@ -120,8 +120,8 @@
<?php // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?> <?php // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?>
<?php foreach ( $post_types as $post_type ) { ?> <?php foreach ( $post_types as $post_type ) { ?>
<li> <li>
<input type="checkbox" id="activitypub_support_post_types" name="activitypub_support_post_types[]" value="<?php echo \esc_attr( $post_type->name ); ?>" <?php echo \checked( true, \in_array( $post_type->name, $support_post_types, true ) ); ?> /> <input type="checkbox" id="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>" name="activitypub_support_post_types[]" value="<?php echo \esc_attr( $post_type->name ); ?>" <?php echo \checked( \in_array( $post_type->name, $support_post_types, true ) ); ?> />
<label for="<?php echo \esc_attr( $post_type->name ); ?>"><?php echo \esc_html( $post_type->label ); ?></label> <label for="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>"><?php echo \esc_html( $post_type->label ); ?></label>
</li> </li>
<?php } ?> <?php } ?>
</ul> </ul>

View file

@ -50,6 +50,7 @@
<p><?php \esc_html_e( 'ActivityPub works as is and there is no need for you to install additional plugins, nevertheless there are some plugins that extends the functionality of ActivityPub.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'ActivityPub works as is and there is no need for you to install additional plugins, nevertheless there are some plugins that extends the functionality of ActivityPub.', 'activitypub' ); ?></p>
<div class="activitypub-settings-accordion"> <div class="activitypub-settings-accordion">
<?php if ( ! \defined( 'FRIENDS_VERSION' ) ) : ?>
<h4 class="activitypub-settings-accordion-heading"> <h4 class="activitypub-settings-accordion-heading">
<button aria-expanded="true" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-friends-plugin" type="button"> <button aria-expanded="true" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-friends-plugin" type="button">
<span class="title"><?php \esc_html_e( 'Following Others', 'activitypub' ); ?></span> <span class="title"><?php \esc_html_e( 'Following Others', 'activitypub' ); ?></span>
@ -58,8 +59,10 @@
</h4> </h4>
<div id="activitypub-settings-accordion-block-friends-plugin" class="activitypub-settings-accordion-panel plugin-card-friends"> <div id="activitypub-settings-accordion-block-friends-plugin" class="activitypub-settings-accordion-panel plugin-card-friends">
<p><?php \esc_html_e( 'To follow people on Mastodon or similar platforms using your own WordPress, you can use the Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress, thus making your own WordPress a Fediverse instance of its own.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'To follow people on Mastodon or similar platforms using your own WordPress, you can use the Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress, thus making your own WordPress a Fediverse instance of its own.', 'activitypub' ); ?></p>
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=friends&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the Friends Plugin for WordPress', 'activitypub' ); ?></a></p> <p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=friends&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the Friends Plugin', 'activitypub' ); ?></a></p>
</div> </div>
<?php endif; ?>
<?php if ( ! \class_exists( 'Hum' ) ) : ?>
<h4 class="activitypub-settings-accordion-heading"> <h4 class="activitypub-settings-accordion-heading">
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-hum-plugin" type="button"> <button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-hum-plugin" type="button">
<span class="title"><?php \esc_html_e( 'Add a URL Shortener', 'activitypub' ); ?></span> <span class="title"><?php \esc_html_e( 'Add a URL Shortener', 'activitypub' ); ?></span>
@ -68,8 +71,10 @@
</h4> </h4>
<div id="activitypub-settings-accordion-block-activitypub-hum-plugin" class="activitypub-settings-accordion-panel plugin-card-hum" hidden="hidden"> <div id="activitypub-settings-accordion-block-activitypub-hum-plugin" class="activitypub-settings-accordion-panel plugin-card-hum" hidden="hidden">
<p><?php \esc_html_e( 'Hum is a personal URL shortener for WordPress, designed to provide short URLs to your personal content, both hosted on WordPress and elsewhere.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'Hum is a personal URL shortener for WordPress, designed to provide short URLs to your personal content, both hosted on WordPress and elsewhere.', 'activitypub' ); ?></p>
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=hum&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install Hum Plugin for WordPress', 'activitypub' ); ?></a></p> <p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=hum&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the Hum Plugin', 'activitypub' ); ?></a></p>
</div> </div>
<?php endif; ?>
<?php if ( ! \class_exists( 'Webfinger' ) ) : ?>
<h4 class="activitypub-settings-accordion-heading"> <h4 class="activitypub-settings-accordion-heading">
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-webfinger-plugin" type="button"> <button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-webfinger-plugin" type="button">
<span class="title"><?php \esc_html_e( 'Advanced WebFinger Support', 'activitypub' ); ?></span> <span class="title"><?php \esc_html_e( 'Advanced WebFinger Support', 'activitypub' ); ?></span>
@ -79,8 +84,10 @@
<div id="activitypub-settings-accordion-block-activitypub-webfinger-plugin" class="activitypub-settings-accordion-panel plugin-card-webfinger" hidden="hidden"> <div id="activitypub-settings-accordion-block-activitypub-webfinger-plugin" class="activitypub-settings-accordion-panel plugin-card-webfinger" hidden="hidden">
<p><?php \esc_html_e( 'WebFinger is a protocol that allows for discovery of information about people and things identified by a URI. Information about a person might be discovered via an "acct:" URI, for example, which is a URI that looks like an email address.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'WebFinger is a protocol that allows for discovery of information about people and things identified by a URI. Information about a person might be discovered via an "acct:" URI, for example, which is a URI that looks like an email address.', 'activitypub' ); ?></p>
<p><?php \esc_html_e( 'The ActivityPub plugin comes with basic WebFinger support, if you need more configuration options and compatibility with other Fediverse/IndieWeb plugins, please install the WebFinger plugin.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'The ActivityPub plugin comes with basic WebFinger support, if you need more configuration options and compatibility with other Fediverse/IndieWeb plugins, please install the WebFinger plugin.', 'activitypub' ); ?></p>
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=webfinger&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install WebFinger Plugin for WordPress', 'activitypub' ); ?></a></p> <p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=webfinger&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the WebFinger Plugin', 'activitypub' ); ?></a></p>
</div> </div>
<?php endif; ?>
<?php if ( ! \function_exists( 'nodeinfo_init' ) ) : ?>
<h4 class="activitypub-settings-accordion-heading"> <h4 class="activitypub-settings-accordion-heading">
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" type="button"> <button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" type="button">
<span class="title"><?php \esc_html_e( 'Provide Enhanced Information about Your Blog', 'activitypub' ); ?></span> <span class="title"><?php \esc_html_e( 'Provide Enhanced Information about Your Blog', 'activitypub' ); ?></span>
@ -90,8 +97,9 @@
<div id="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" class="activitypub-settings-accordion-panel plugin-card-nodeinfo" hidden="hidden"> <div id="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" class="activitypub-settings-accordion-panel plugin-card-nodeinfo" hidden="hidden">
<p><?php \esc_html_e( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ); ?></p>
<p><?php \esc_html_e( 'The ActivityPub plugin comes with a simple NodeInfo endpoint. If you need more configuration options and compatibility with other Fediverse plugins, please install the NodeInfo plugin.', 'activitypub' ); ?></p> <p><?php \esc_html_e( 'The ActivityPub plugin comes with a simple NodeInfo endpoint. If you need more configuration options and compatibility with other Fediverse plugins, please install the NodeInfo plugin.', 'activitypub' ); ?></p>
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=nodeinfo&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install NodeInfo Plugin for WordPress', 'activitypub' ); ?></a></p> <p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=nodeinfo&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the NodeInfo Plugin', 'activitypub' ); ?></a></p>
</div> </div>
<?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>

View file

@ -22,7 +22,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
$activitypub_activity = new \Activitypub\Model\Activity( 'Create' ); $activitypub_activity = new \Activitypub\Model\Activity( 'Create' );
$activitypub_activity->from_post( $activitypub_post ); $activitypub_activity->from_post( $activitypub_post );
$this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_to() ); $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() ); $this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() );
remove_all_filters( 'activitypub_extract_mentions' ); remove_all_filters( 'activitypub_extract_mentions' );

View file

@ -58,6 +58,13 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertEquals( 3, \count( $db_followers ) ); $this->assertEquals( 3, \count( $db_followers ) );
$db_followers = array_map(
function( $item ) {
return $item->get_actor();
},
$db_followers
);
$this->assertSame( array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ), $db_followers ); $this->assertSame( array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' ), $db_followers );
} }
@ -90,6 +97,60 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
$this->assertNull( $follower ); $this->assertNull( $follower );
} }
public function test_get_outdated_followers() {
$followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' );
$pre_http_request = new MockAction();
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
foreach ( $followers as $follower ) {
\Activitypub\Collection\Followers::add_follower( 1, $follower );
}
$follower = new \Activitypub\Model\Follower( 'https://example.com/author/jon' );
update_term_meta( $follower->get_id(), 'updated_at', \time() - 804800 );
$followers = \Activitypub\Collection\Followers::get_outdated_followers();
$this->assertEquals( 1, count( $followers ) );
$this->assertEquals( 'https://example.com/author/jon', $followers[0] );
}
public function test_get_faulty_followers() {
$followers = array( 'https://example.com/author/jon', 'https://example.org/author/doe', 'http://sally.example.org' );
$pre_http_request = new MockAction();
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
foreach ( $followers as $follower ) {
\Activitypub\Collection\Followers::add_follower( 1, $follower );
}
$follower = new \Activitypub\Model\Follower( 'http://sally.example.org' );
for ( $i = 1; $i <= 15; $i++ ) {
add_term_meta( $follower->get_id(), 'errors', 'error ' . $i );
}
$follower = new \Activitypub\Model\Follower( 'http://sally.example.org' );
$count = $follower->count_errors();
$followers = \Activitypub\Collection\Followers::get_faulty_followers();
$this->assertEquals( 1, count( $followers ) );
$this->assertEquals( 'http://sally.example.org', $followers[0] );
$follower->reset_errors();
$follower = new \Activitypub\Model\Follower( 'http://sally.example.org' );
$count = $follower->count_errors();
$followers = \Activitypub\Collection\Followers::get_faulty_followers();
$this->assertEquals( 0, count( $followers ) );
}
public static function http_request_host_is_external( $in, $host ) { public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) { if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
return true; return true;