modified file plugins
This commit is contained in:
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Activity_Dispatcher Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Activity_Dispatcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_send_post_activity', array( '\Activitypub\Activity_Dispatcher', 'send_post_activity' ) );
|
||||
\add_action( 'activitypub_send_update_activity', array( '\Activitypub\Activity_Dispatcher', 'send_update_activity' ) );
|
||||
\add_action( 'activitypub_send_delete_activity', array( '\Activitypub\Activity_Dispatcher', 'send_delete_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send "create" activities.
|
||||
*
|
||||
* @param \Activitypub\Model\Post $activitypub_post
|
||||
*/
|
||||
public static function send_post_activity( Model\Post $activitypub_post ) {
|
||||
// get latest version of post
|
||||
$user_id = $activitypub_post->get_post_author();
|
||||
|
||||
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
|
||||
$activitypub_activity->from_post( $activitypub_post );
|
||||
|
||||
$inboxes = \Activitypub\get_follower_inboxes( $user_id );
|
||||
|
||||
$followers_url = \get_rest_url( null, '/activitypub/1.0/users/' . intval( $user_id ) . '/followers' );
|
||||
foreach ( $activitypub_activity->get_cc() as $cc ) {
|
||||
if ( $cc === $followers_url ) {
|
||||
continue;
|
||||
}
|
||||
$inbox = \Activitypub\get_inbox_by_actor( $cc );
|
||||
if ( ! $inbox || \is_wp_error( $inbox ) ) {
|
||||
continue;
|
||||
}
|
||||
// init array if empty
|
||||
if ( ! isset( $inboxes[ $inbox ] ) ) {
|
||||
$inboxes[ $inbox ] = array();
|
||||
}
|
||||
$inboxes[ $inbox ][] = $cc;
|
||||
}
|
||||
|
||||
foreach ( $inboxes as $inbox => $to ) {
|
||||
$to = array_values( array_unique( $to ) );
|
||||
$activitypub_activity->set_to( $to );
|
||||
$activity = $activitypub_activity->to_json();
|
||||
|
||||
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send "update" activities.
|
||||
*
|
||||
* @param \Activitypub\Model\Post $activitypub_post
|
||||
*/
|
||||
public static function send_update_activity( $activitypub_post ) {
|
||||
// get latest version of post
|
||||
$user_id = $activitypub_post->get_post_author();
|
||||
|
||||
$activitypub_activity = new \Activitypub\Model\Activity( 'Update', \Activitypub\Model\Activity::TYPE_FULL );
|
||||
$activitypub_activity->from_post( $activitypub_post );
|
||||
|
||||
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) {
|
||||
$activitypub_activity->set_to( $to );
|
||||
$activity = $activitypub_activity->to_json(); // phpcs:ignore
|
||||
|
||||
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send "delete" activities.
|
||||
*
|
||||
* @param \Activitypub\Model\Post $activitypub_post
|
||||
*/
|
||||
public static function send_delete_activity( $activitypub_post ) {
|
||||
// get latest version of post
|
||||
$user_id = $activitypub_post->get_post_author();
|
||||
|
||||
$activitypub_activity = new \Activitypub\Model\Activity( 'Delete', \Activitypub\Model\Activity::TYPE_FULL );
|
||||
$activitypub_activity->from_post( $activitypub_post );
|
||||
|
||||
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) {
|
||||
$activitypub_activity->set_to( $to );
|
||||
$activity = $activitypub_activity->to_json(); // phpcs:ignore
|
||||
|
||||
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Activitypub {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 );
|
||||
\add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) );
|
||||
\add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 );
|
||||
|
||||
// Add support for ActivityPub to custom post types
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array();
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
\add_post_type_support( $post_type, 'activitypub' );
|
||||
}
|
||||
|
||||
\add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 33, 3 );
|
||||
\add_action( 'wp_trash_post', array( '\Activitypub\Activitypub', 'trash_post' ), 1 );
|
||||
\add_action( 'untrash_post', array( '\Activitypub\Activitypub', 'untrash_post' ), 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a AS2 JSON version of an author, post or page.
|
||||
*
|
||||
* @param string $template The path to the template object.
|
||||
*
|
||||
* @return string The new path to the JSON template.
|
||||
*/
|
||||
public static function render_json_template( $template ) {
|
||||
if ( ! \is_author() && ! \is_singular() && ! \is_home() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
// check if user can publish posts
|
||||
if ( \is_author() && ! user_can( \get_the_author_meta( 'ID' ), 'publish_posts' ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
||||
} elseif ( \is_singular() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
||||
} elseif ( \is_home() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
|
||||
if ( isset( $wp_query->query_vars['activitypub'] ) ) {
|
||||
return $json_template;
|
||||
}
|
||||
|
||||
if ( ! isset( $_SERVER['HTTP_ACCEPT'] ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$accept_header = $_SERVER['HTTP_ACCEPT'];
|
||||
|
||||
if (
|
||||
\stristr( $accept_header, 'application/activity+json' ) ||
|
||||
\stristr( $accept_header, 'application/ld+json' )
|
||||
) {
|
||||
return $json_template;
|
||||
}
|
||||
|
||||
// Accept header as an array.
|
||||
$accept = \explode( ',', \trim( $accept_header ) );
|
||||
|
||||
if (
|
||||
\in_array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', $accept, true ) ||
|
||||
\in_array( 'application/activity+json', $accept, true ) ||
|
||||
\in_array( 'application/ld+json', $accept, true ) ||
|
||||
\in_array( 'application/json', $accept, true )
|
||||
) {
|
||||
return $json_template;
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||
*/
|
||||
public static function add_query_vars( $vars ) {
|
||||
$vars[] = 'activitypub';
|
||||
|
||||
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_post_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.
|
||||
*
|
||||
* @param array $args Arguments passed to get_avatar_data(), after processing.
|
||||
* @param int|string|object $id_or_email A user ID, email address, or comment object.
|
||||
*
|
||||
* @return array $args
|
||||
*/
|
||||
public static function pre_get_avatar_data( $args, $id_or_email ) {
|
||||
if (
|
||||
! $id_or_email instanceof \WP_Comment ||
|
||||
! isset( $id_or_email->comment_type ) ||
|
||||
$id_or_email->user_id
|
||||
) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$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 ) ) {
|
||||
$args['url'] = false;
|
||||
/** This filter is documented in wp-includes/link-template.php */
|
||||
return \apply_filters( 'get_avatar_data', $args, $id_or_email );
|
||||
}
|
||||
|
||||
// Check if comment has an avatar.
|
||||
$avatar = self::get_avatar_url( $id_or_email->comment_ID );
|
||||
|
||||
if ( $avatar ) {
|
||||
if ( ! isset( $args['class'] ) || ! \is_array( $args['class'] ) ) {
|
||||
$args['class'] = array( 'u-photo' );
|
||||
} else {
|
||||
$args['class'][] = 'u-photo';
|
||||
$args['class'] = \array_unique( $args['class'] );
|
||||
}
|
||||
$args['url'] = $avatar;
|
||||
$args['class'][] = 'avatar-activitypub';
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to retrieve Avatar URL if stored in meta.
|
||||
*
|
||||
* @param int|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function get_avatar_url( $comment ) {
|
||||
if ( \is_numeric( $comment ) ) {
|
||||
$comment = \get_comment( $comment );
|
||||
}
|
||||
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store permalink in meta, to send delete Activity
|
||||
*
|
||||
* @param string $post_id The Post ID
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function trash_post( $post_id ) {
|
||||
\add_post_meta( $post_id, 'activitypub_canonical_url', \get_permalink( $post_id ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete permalink from meta
|
||||
*
|
||||
* @param string $post_id The Post ID
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function untrash_post( $post_id ) {
|
||||
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Admin Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Admin {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'admin_menu', array( '\Activitypub\Admin', 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( '\Activitypub\Admin', 'register_settings' ) );
|
||||
\add_action( 'show_user_profile', array( '\Activitypub\Admin', 'add_fediverse_profile' ) );
|
||||
\add_action( 'admin_enqueue_scripts', array( '\Activitypub\Admin', 'enqueue_scripts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu entry
|
||||
*/
|
||||
public static function admin_menu() {
|
||||
$settings_page = \add_options_page(
|
||||
'Welcome',
|
||||
'ActivityPub',
|
||||
'manage_options',
|
||||
'activitypub',
|
||||
array( '\Activitypub\Admin', 'settings_page' )
|
||||
);
|
||||
|
||||
\add_action( 'load-' . $settings_page, array( '\Activitypub\Admin', 'add_settings_help_tab' ) );
|
||||
|
||||
$followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers (Fediverse)', 'activitypub' ), 'read', 'activitypub-followers-list', array( '\Activitypub\Admin', 'followers_list_page' ) );
|
||||
|
||||
\add_action( 'load-' . $followers_list_page, array( '\Activitypub\Admin', 'add_followers_list_help_tab' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings page
|
||||
*/
|
||||
public static function settings_page() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['tab'] ) ) {
|
||||
$tab = 'welcome';
|
||||
} else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tab = sanitize_key( $_GET['tab'] );
|
||||
}
|
||||
|
||||
switch ( $tab ) {
|
||||
case 'settings':
|
||||
\Activitypub\Model\Post::upgrade_post_content_template();
|
||||
|
||||
\load_template( \dirname( __FILE__ ) . '/../templates/settings.php' );
|
||||
break;
|
||||
case 'welcome':
|
||||
default:
|
||||
wp_enqueue_script( 'plugin-install' );
|
||||
add_thickbox();
|
||||
wp_enqueue_script( 'updates' );
|
||||
|
||||
\load_template( \dirname( __FILE__ ) . '/../templates/welcome.php' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user settings page
|
||||
*/
|
||||
public static function followers_list_page() {
|
||||
\load_template( \dirname( __FILE__ ) . '/../templates/followers-list.php' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register ActivityPub settings
|
||||
*/
|
||||
public static function register_settings() {
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_post_content_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array( 'title', 'excerpt', 'content' ),
|
||||
),
|
||||
),
|
||||
'default' => 'content',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_custom_post_content',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Define your own custom post template', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_max_image_attachments',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => \__( 'Number of images to attach to posts.', 'activitypub' ),
|
||||
'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_object_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'The Activity-Object-Type', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array( 'note', 'article', 'wordpress-post-format' ),
|
||||
),
|
||||
),
|
||||
'default' => 'note',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_use_hashtags',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ),
|
||||
'default' => 0,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_support_post_types',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => array( 'post', 'pages' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function add_settings_help_tab() {
|
||||
require_once \dirname( __FILE__ ) . '/help.php';
|
||||
}
|
||||
|
||||
public static function add_followers_list_help_tab() {
|
||||
// todo
|
||||
}
|
||||
|
||||
public static function add_fediverse_profile( $user ) {
|
||||
?>
|
||||
<h2 id="activitypub"><?php \esc_html_e( 'ActivityPub', 'activitypub' ); ?></h2>
|
||||
<?php
|
||||
\Activitypub\get_identifier_settings( $user->ID );
|
||||
}
|
||||
|
||||
public static function enqueue_scripts( $hook_suffix ) {
|
||||
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
|
||||
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), '1.0.0' );
|
||||
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.0', false );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Debug Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Debug {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
if ( WP_DEBUG && WP_DEBUG_LOG ) {
|
||||
\add_action( 'activitypub_safe_remote_post_response', array( '\Activitypub\Debug', 'log_remote_post_responses' ), 10, 4 );
|
||||
}
|
||||
}
|
||||
|
||||
public static function log_remote_post_responses( $response, $url, $body, $user_id ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
\error_log( "Request to: {$url} with response: " . \print_r( $response, true ) );
|
||||
}
|
||||
|
||||
public static function write_log( $log ) {
|
||||
if ( \is_array( $log ) || \is_object( $log ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
\error_log( \print_r( $log, true ) );
|
||||
} else {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
\error_log( $log );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Hashtag Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Hashtag {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
|
||||
\add_filter( 'wp_insert_post', array( '\Activitypub\Hashtag', 'insert_post' ), 10, 2 );
|
||||
\add_filter( 'the_content', array( '\Activitypub\Hashtag', 'the_content' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to save #tags as real WordPress tags
|
||||
*
|
||||
* @param int $id the rev-id
|
||||
* @param WP_Post $post the post
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static function insert_post( $id, $post ) {
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
|
||||
$tags = \implode( ', ', $match[1] );
|
||||
|
||||
\wp_add_post_tags( $post->post_parent, $tags );
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the #tags in the content with links
|
||||
*
|
||||
* @param string $the_content the post-content
|
||||
*
|
||||
* @return string the filtered post-content
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
$protected_tags = array();
|
||||
$protect = function( $m ) use ( &$protected_tags ) {
|
||||
$c = count( $protected_tags );
|
||||
$protect = '!#!#PROTECT' . $c . '#!#!';
|
||||
$protected_tags[ $protect ] = $m[0];
|
||||
return $protect;
|
||||
};
|
||||
$the_content = preg_replace_callback(
|
||||
'#<!\[CDATA\[.*?\]\]>#is',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
$the_content = preg_replace_callback(
|
||||
'#<(pre|code|textarea|style)\b[^>]*>.*?</\1[^>]*>#is',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
$the_content = preg_replace_callback(
|
||||
'#<[^>]+>#i',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
|
||||
$the_content = \preg_replace_callback( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( '\Activitypub\Hashtag', 'replace_with_links' ), $the_content );
|
||||
|
||||
$the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content );
|
||||
|
||||
return $the_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the term links
|
||||
*
|
||||
* @param array $result the preg_match results
|
||||
* @return string the final string
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
$tag = $result[1];
|
||||
$tag_object = \get_term_by( 'name', $tag, 'post_tag' );
|
||||
|
||||
if ( $tag_object ) {
|
||||
$link = \get_term_link( $tag_object, 'post_tag' );
|
||||
return \sprintf( '<a rel="tag" class="u-tag u-category" href="%s">#%s</a>', $link, $tag );
|
||||
}
|
||||
|
||||
return '#' . $tag;
|
||||
}
|
||||
}
|
@ -0,0 +1,296 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Health_Check Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Health_Check {
|
||||
|
||||
/**
|
||||
* Initialize health checks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'site_status_tests', array( '\Activitypub\Health_Check', 'add_tests' ) );
|
||||
\add_filter( 'debug_information', array( '\Activitypub\Health_Check', 'debug_information' ) );
|
||||
}
|
||||
|
||||
public static function add_tests( $tests ) {
|
||||
$tests['direct']['activitypub_test_author_url'] = array(
|
||||
'label' => \__( 'Author URL test', 'activitypub' ),
|
||||
'test' => array( '\Activitypub\Health_Check', 'test_author_url' ),
|
||||
);
|
||||
|
||||
$tests['direct']['activitypub_test_webfinger'] = array(
|
||||
'label' => __( 'WebFinger Test', 'activitypub' ),
|
||||
'test' => array( '\Activitypub\Health_Check', 'test_webfinger' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author URL tests
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function test_author_url() {
|
||||
$result = array(
|
||||
'label' => \__( 'Author URL accessible', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_author_url',
|
||||
);
|
||||
|
||||
$check = self::is_author_url_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Author URL is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFinger tests
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function test_webfinger() {
|
||||
$result = array(
|
||||
'label' => \__( 'WebFinger endpoint', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your WebFinger endpoint is accessible and returns the correct informations.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_webfinger',
|
||||
);
|
||||
|
||||
$check = self::is_webfinger_endpoint_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `author_posts_url` is accessible and that requerst returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_author_url_accessible() {
|
||||
$user = \wp_get_current_user();
|
||||
$author_url = \get_author_posts_url( $user->ID );
|
||||
$reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename );
|
||||
|
||||
// check for "author" in URL
|
||||
if ( $author_url !== $reference_author_url ) {
|
||||
return new \WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your author URL <code>%s</code> was replaced, this is often done by plugins.</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// try to access author URL
|
||||
$response = \wp_remote_get(
|
||||
$author_url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||
'redirection' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return new \WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your author URL <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response_code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// check for redirects
|
||||
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
|
||||
return new \WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your author URL <code>%s</code> is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// check if response is JSON
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
|
||||
return new \WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your author URL <code>%s</code> does not return valid JSON for <code>application/activity+json</code>. Please check if your hosting supports alternate <code>Accept</code> headers.</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebFinger endoint is accessible and profile requerst returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_webfinger_endpoint_accessible() {
|
||||
$user = \wp_get_current_user();
|
||||
$account = \Activitypub\get_webfinger_resource( $user->ID );
|
||||
|
||||
$url = \Activitypub\Webfinger::resolve( $account );
|
||||
if ( \is_wp_error( $url ) ) {
|
||||
$health_messages = array(
|
||||
'webfinger_url_not_accessible' => \sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$url->get_error_data()
|
||||
),
|
||||
'webfinger_url_invalid_response' => \sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'<p>Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.</p>',
|
||||
'activitypub'
|
||||
),
|
||||
$url->get_error_data()
|
||||
),
|
||||
);
|
||||
$message = null;
|
||||
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
|
||||
$message = $health_messages[ $url->get_error_code() ];
|
||||
}
|
||||
return new \WP_Error(
|
||||
$url->get_error_code(),
|
||||
$message,
|
||||
$url->get_error_data()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URL to the author page for the user with the ID provided.
|
||||
*
|
||||
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
|
||||
*
|
||||
* @param int $author_id Author ID.
|
||||
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
|
||||
*
|
||||
* @return string The URL to the author's page.
|
||||
*/
|
||||
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
|
||||
global $wp_rewrite;
|
||||
$auth_id = (int) $author_id;
|
||||
$link = $wp_rewrite->get_author_permastruct();
|
||||
|
||||
if ( empty( $link ) ) {
|
||||
$file = home_url( '/' );
|
||||
$link = $file . '?author=' . $auth_id;
|
||||
} else {
|
||||
if ( '' === $author_nicename ) {
|
||||
$user = get_userdata( $author_id );
|
||||
if ( ! empty( $user->user_nicename ) ) {
|
||||
$author_nicename = $user->user_nicename;
|
||||
}
|
||||
}
|
||||
$link = str_replace( '%author%', $author_nicename, $link );
|
||||
$link = home_url( user_trailingslashit( $link ) );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function for generating site debug data when required.
|
||||
*
|
||||
* @param array $info The debug information to be added to the core information page.
|
||||
* @return array The filtered informations
|
||||
*/
|
||||
public static function debug_information( $info ) {
|
||||
$info['activitypub'] = array(
|
||||
'label' => __( 'ActivityPub', 'activitypub' ),
|
||||
'fields' => array(
|
||||
'webfinger' => array(
|
||||
'label' => __( 'WebFinger Resource', 'activitypub' ),
|
||||
'value' => \Activitypub\Webfinger::get_user_resource( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'author_url' => array(
|
||||
'label' => __( 'Author URL', 'activitypub' ),
|
||||
'value' => get_author_posts_url( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Mention Class
|
||||
*
|
||||
* @author Alex Kirk
|
||||
*/
|
||||
class Mention {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'the_content', array( '\Activitypub\Mention', 'the_content' ), 99, 2 );
|
||||
\add_filter( 'activitypub_extract_mentions', array( '\Activitypub\Mention', 'extract_mentions' ), 99, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the mentions in the content with links
|
||||
*
|
||||
* @param string $the_content the post-content
|
||||
*
|
||||
* @return string the filtered post-content
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
$protected_tags = array();
|
||||
$protect = function( $m ) use ( &$protected_tags ) {
|
||||
$c = count( $protected_tags );
|
||||
$protect = '!#!#PROTECT' . $c . '#!#!';
|
||||
$protected_tags[ $protect ] = $m[0];
|
||||
return $protect;
|
||||
};
|
||||
$the_content = preg_replace_callback(
|
||||
'#<!\[CDATA\[.*?\]\]>#is',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
$the_content = preg_replace_callback(
|
||||
'#<(pre|code|textarea|style)\b[^>]*>.*?</\1[^>]*>#is',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
$the_content = preg_replace_callback(
|
||||
'#<a.*?href=[^>]+>.*?</a>#i',
|
||||
$protect,
|
||||
$the_content
|
||||
);
|
||||
|
||||
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( '\Activitypub\Mention', 'replace_with_links' ), $the_content );
|
||||
|
||||
$the_content = str_replace( array_reverse( array_keys( $protected_tags ) ), array_reverse( array_values( $protected_tags ) ), $the_content );
|
||||
|
||||
return $the_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the user links
|
||||
*
|
||||
* @param array $result the preg_match results
|
||||
* @return string the final string
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
$metadata = \ActivityPub\get_remote_metadata_by_actor( $result[0] );
|
||||
if ( ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
|
||||
$username = ltrim( $result[0], '@' );
|
||||
if ( ! empty( $metadata['name'] ) ) {
|
||||
$username = $metadata['name'];
|
||||
}
|
||||
if ( ! empty( $metadata['preferredUsername'] ) ) {
|
||||
$username = $metadata['preferredUsername'];
|
||||
}
|
||||
$username = '@<span>' . $username . '</span>';
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">%s</a>', $metadata['url'], $username );
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the mentions from the post_content.
|
||||
*
|
||||
* @param array $mentions The already found mentions.
|
||||
* @param string $post_content The post content.
|
||||
* @return mixed The discovered mentions.
|
||||
*/
|
||||
public static function extract_mentions( $mentions, $post_content ) {
|
||||
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
|
||||
foreach ( $matches[0] as $match ) {
|
||||
$link = \Activitypub\Webfinger::resolve( $match );
|
||||
if ( ! is_wp_error( $link ) ) {
|
||||
$mentions[ $match ] = $link;
|
||||
}
|
||||
}
|
||||
return $mentions;
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,527 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
class Shortcodes {
|
||||
/**
|
||||
* Class constructor, registering WordPress then shortcodes
|
||||
*
|
||||
* @param WP_Post $post A WordPress Post Object
|
||||
*/
|
||||
public static function init() {
|
||||
foreach ( get_class_methods( 'Activitypub\Shortcodes' ) as $shortcode ) {
|
||||
if ( 'init' !== $shortcode ) {
|
||||
add_shortcode( 'ap_' . $shortcode, array( 'Activitypub\Shortcodes', $shortcode ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_hashtags shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function hashtags( $atts, $content, $tag ) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tags = \get_the_tags( $post_id );
|
||||
|
||||
if ( ! $tags ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="u-tag u-category" href="%s">#%s</a>',
|
||||
\get_tag_link( $tag ),
|
||||
$tag->slug
|
||||
);
|
||||
}
|
||||
|
||||
return \implode( ' ', $hash_tags );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_title shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function title( $atts, $content, $tag ) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \get_the_title( $post_id );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_excerpt shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function excerpt( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post || \post_password_required( $post ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'length' => ACTIVITYPUB_EXCERPT_LENGTH ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$excerpt_length = intval( $atts['length'] );
|
||||
|
||||
if ( 0 === $excerpt_length ) {
|
||||
$excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH;
|
||||
}
|
||||
|
||||
$excerpt = \get_post_field( 'post_excerpt', $post );
|
||||
|
||||
if ( '' === $excerpt ) {
|
||||
|
||||
$content = \get_post_field( 'post_content', $post );
|
||||
|
||||
// An empty string will make wp_trim_excerpt do stuff we do not want.
|
||||
if ( '' !== $content ) {
|
||||
$excerpt = \strip_shortcodes( $content );
|
||||
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
$excerpt = \apply_filters( 'the_content', $excerpt );
|
||||
$excerpt = \str_replace( ']]>', ']]>', $excerpt );
|
||||
}
|
||||
}
|
||||
|
||||
// Strip out any remaining tags.
|
||||
$excerpt = \wp_strip_all_tags( $excerpt );
|
||||
|
||||
/** This filter is documented in wp-includes/formatting.php */
|
||||
$excerpt_more = \apply_filters( 'excerpt_more', ' [...]' );
|
||||
$excerpt_more_len = strlen( $excerpt_more );
|
||||
|
||||
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:
|
||||
//
|
||||
// * The user has entered a manual excerpt which is longer that what we want.
|
||||
// * No manual excerpt exists so we've used the content which might be longer than we want.
|
||||
//
|
||||
// Either way, let's trim it up if we need too. Also, don't forget to take into account the more indicator
|
||||
// as part of the total length.
|
||||
//
|
||||
|
||||
// Setup a variable to hold the current excerpts length.
|
||||
$current_excerpt_length = strlen( $excerpt );
|
||||
|
||||
// Setup a variable to keep track of our target length.
|
||||
$target_excerpt_length = $excerpt_length - $excerpt_more_len;
|
||||
|
||||
// Setup a variable to keep track of the current max length.
|
||||
$current_excerpt_max = $target_excerpt_length;
|
||||
|
||||
// This is a loop since we can't calculate word break the string after 'the_excpert' filter has run (we would break
|
||||
// all kinds of html tags), so we have to cut the excerpt down a bit at a time until we hit our target length.
|
||||
while ( $current_excerpt_length > $target_excerpt_length && $current_excerpt_max > 0 ) {
|
||||
// Trim the excerpt based on wordwrap() positioning.
|
||||
// Note: we're using <br> as the linebreak just in case there are any newlines existing in the excerpt from the user.
|
||||
// There won't be any <br> left after we've run wp_strip_all_tags() in the code above, so they're
|
||||
// safe to use here. It won't be included in the final excerpt as the substr() will trim it off.
|
||||
$excerpt = substr( $excerpt, 0, strpos( wordwrap( $excerpt, $current_excerpt_max, '<br>' ), '<br>' ) );
|
||||
|
||||
// If something went wrong, or we're in a language that wordwrap() doesn't understand,
|
||||
// just chop it off and don't worry about breaking in the middle of a word.
|
||||
if ( strlen( $excerpt ) > $excerpt_length - $excerpt_more_len ) {
|
||||
$excerpt = substr( $excerpt, 0, $current_excerpt_max );
|
||||
}
|
||||
|
||||
// Add in the more indicator.
|
||||
$excerpt = $excerpt . $excerpt_more;
|
||||
|
||||
// Run it through the excerpt filter which will add some html tags back in.
|
||||
$excerpt_filtered = apply_filters( 'the_excerpt', $excerpt );
|
||||
|
||||
// Now set the current excerpt length to this new filtered length.
|
||||
$current_excerpt_length = strlen( $excerpt_filtered );
|
||||
|
||||
// Check to see if we're over the target length.
|
||||
if ( $current_excerpt_length > $target_excerpt_length ) {
|
||||
// If so, remove 20 characters from the current max and run the loop again.
|
||||
$current_excerpt_max = $current_excerpt_max - 20;
|
||||
}
|
||||
}
|
||||
|
||||
return \apply_filters( 'the_excerpt', $excerpt );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_content shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function content( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post || \post_password_required( $post ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'apply_filters' => 'yes' ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$content = \get_post_field( 'post_content', $post );
|
||||
|
||||
if ( 'yes' === $atts['apply_filters'] ) {
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
} else {
|
||||
$content = do_blocks( $content );
|
||||
$content = wptexturize( $content );
|
||||
$content = wp_filter_content_tags( $content );
|
||||
}
|
||||
|
||||
// replace script and style elements
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_permalink shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function permalink( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'url',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
return \esc_url( \get_permalink( $post->ID ) );
|
||||
}
|
||||
|
||||
return \sprintf( '<a href="%1$s">%1$s</a>', \esc_url( \get_permalink( $post->ID ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_shortlink shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function shortlink( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'url',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
return \esc_url( \wp_get_shortlink( $post->ID ) );
|
||||
}
|
||||
|
||||
return \sprintf( '<a href="%1$s">%1$s</a>', \esc_url( \wp_get_shortlink( $post->ID ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_image shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function image( $atts, $content, $tag ) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'full',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$size = 'full';
|
||||
|
||||
if ( in_array(
|
||||
$atts['type'],
|
||||
array( 'thumbnail', 'medium', 'large', 'full' ),
|
||||
true
|
||||
) ) {
|
||||
$size = $atts['type'];
|
||||
}
|
||||
|
||||
$image = \get_the_post_thumbnail_url( $post_id, $size );
|
||||
|
||||
if ( ! $image ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \esc_url( $image );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_hashcats shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function hashcats( $atts, $content, $tag ) {
|
||||
$post_id = get_the_ID();
|
||||
|
||||
if ( ! $post_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$categories = \get_the_category( $post_id );
|
||||
|
||||
if ( ! $categories ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $categories as $category ) {
|
||||
$hash_tags[] = \sprintf( '<a rel="tag" class="u-tag u-category" href="%s">#%s</a>', \get_category_link( $category ), $category->slug );
|
||||
}
|
||||
|
||||
return \implode( ' ', $hash_tags );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_author shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function author( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$name = \get_the_author_meta( 'display_name', $post->post_author );
|
||||
|
||||
if ( ! $name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_authorurl shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function authorurl( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = \get_the_author_meta( 'user_url', $post->post_author );
|
||||
|
||||
if ( ! $url ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \esc_url( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_blogurl shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function blogurl( $atts, $content, $tag ) {
|
||||
return \esc_url( \get_bloginfo( 'url' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_blogname shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function blogname( $atts, $content, $tag ) {
|
||||
return \get_bloginfo( 'name' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_blogdesc shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function blogdesc( $atts, $content, $tag ) {
|
||||
return \get_bloginfo( 'description' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_date shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function date( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $post );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $dateformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_time shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function time( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $post );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $timeformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the ap_datetime shortcode
|
||||
*
|
||||
* @param array $atts shortcode attributes
|
||||
* @param string $content shortcode content
|
||||
* @param string $tag shortcode tag name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function datetime( $atts, $content, $tag ) {
|
||||
$post = get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $post );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $dateformat . ' @ ' . $timeformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Signature Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Signature {
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_public_key( $user_id, $force = false ) {
|
||||
$key = \get_user_meta( $user_id, 'magic_sig_public_key' );
|
||||
|
||||
if ( $key && ! $force ) {
|
||||
return $key[0];
|
||||
}
|
||||
|
||||
self::generate_key_pair( $user_id );
|
||||
$key = \get_user_meta( $user_id, 'magic_sig_public_key' );
|
||||
|
||||
return $key[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get_private_key( $user_id, $force = false ) {
|
||||
$key = \get_user_meta( $user_id, 'magic_sig_private_key' );
|
||||
|
||||
if ( $key && ! $force ) {
|
||||
return $key[0];
|
||||
}
|
||||
|
||||
self::generate_key_pair( $user_id );
|
||||
$key = \get_user_meta( $user_id, 'magic_sig_private_key' );
|
||||
|
||||
return $key[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the pair keys
|
||||
*
|
||||
* @param int $user_id
|
||||
*/
|
||||
public static function generate_key_pair( $user_id ) {
|
||||
$config = array(
|
||||
'digest_alg' => 'sha512',
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => \OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$key = \openssl_pkey_new( $config );
|
||||
$priv_key = null;
|
||||
|
||||
\openssl_pkey_export( $key, $priv_key );
|
||||
|
||||
// private key
|
||||
\update_user_meta( $user_id, 'magic_sig_private_key', $priv_key );
|
||||
|
||||
$detail = \openssl_pkey_get_details( $key );
|
||||
|
||||
// public key
|
||||
\update_user_meta( $user_id, 'magic_sig_public_key', $detail['key'] );
|
||||
}
|
||||
|
||||
public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) {
|
||||
$key = self::get_private_key( $user_id );
|
||||
|
||||
$url_parts = \wp_parse_url( $url );
|
||||
|
||||
$host = $url_parts['host'];
|
||||
$path = '/';
|
||||
|
||||
// add path
|
||||
if ( ! empty( $url_parts['path'] ) ) {
|
||||
$path = $url_parts['path'];
|
||||
}
|
||||
|
||||
// add query
|
||||
if ( ! empty( $url_parts['query'] ) ) {
|
||||
$path .= '?' . $url_parts['query'];
|
||||
}
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
$signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: SHA-256=$digest";
|
||||
} else {
|
||||
$signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date";
|
||||
}
|
||||
|
||||
$signature = null;
|
||||
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
|
||||
$signature = \base64_encode( $signature ); // phpcs:ignore
|
||||
|
||||
$key_id = \get_author_posts_url( $user_id ) . '#main-key';
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature );
|
||||
} else {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date",signature="%s"', $key_id, $signature );
|
||||
}
|
||||
}
|
||||
|
||||
public static function verify_signature( $headers, $signature ) {
|
||||
|
||||
}
|
||||
|
||||
public static function generate_digest( $body ) {
|
||||
$digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore
|
||||
return "$digest";
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://webfinger.net/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Returns a users WebFinger "resource"
|
||||
*
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return string The user-resource
|
||||
*/
|
||||
public static function get_user_resource( $user_id ) {
|
||||
// use WebFinger plugin if installed
|
||||
if ( \function_exists( '\get_webfinger_resource' ) ) {
|
||||
return \get_webfinger_resource( $user_id, false );
|
||||
}
|
||||
|
||||
$user = \get_user_by( 'id', $user_id );
|
||||
|
||||
return $user->user_login . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
|
||||
}
|
||||
|
||||
public static function resolve( $account ) {
|
||||
if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $account, $m ) ) {
|
||||
return null;
|
||||
}
|
||||
$transient_key = 'activitypub_resolve_' . ltrim( $account, '@' );
|
||||
|
||||
$link = \get_transient( $transient_key );
|
||||
if ( $link ) {
|
||||
return $link;
|
||||
}
|
||||
|
||||
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
|
||||
if ( ! \wp_http_validate_url( $url ) ) {
|
||||
$response = new \WP_Error( 'invalid_webfinger_url', null, $url );
|
||||
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $response;
|
||||
}
|
||||
|
||||
// try to access author URL
|
||||
$response = \wp_remote_get(
|
||||
$url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||
'redirection' => 0,
|
||||
'timeout' => 2,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
$link = new \WP_Error( 'webfinger_url_not_accessible', null, $url );
|
||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $link;
|
||||
}
|
||||
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
$body = \json_decode( $body, true );
|
||||
|
||||
if ( empty( $body['links'] ) ) {
|
||||
$link = new \WP_Error( 'webfinger_url_invalid_response', null, $url );
|
||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $link;
|
||||
}
|
||||
|
||||
foreach ( $body['links'] as $link ) {
|
||||
if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
|
||||
\set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
|
||||
return $link['href'];
|
||||
}
|
||||
}
|
||||
|
||||
$link = new \WP_Error( 'webfinger_url_no_activity_pub', null, $body );
|
||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $link;
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Allow localhost URLs if WP_DEBUG is true.
|
||||
*
|
||||
* @param array $r Array of HTTP request args.
|
||||
* @param string $url The request URL.
|
||||
* @return array $args Array or string of HTTP request arguments.
|
||||
*/
|
||||
function allow_localhost( $r, $url ) {
|
||||
$r['reject_unsafe_urls'] = false;
|
||||
|
||||
return $r;
|
||||
}
|
||||
add_filter( 'http_request_args', '\Activitypub\allow_localhost', 10, 2 );
|
@ -0,0 +1,322 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Returns the ActivityPub default JSON-context
|
||||
*
|
||||
* @return array the activitypub context
|
||||
*/
|
||||
function get_context() {
|
||||
$context = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
array(
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'schema' => 'http://schema.org#',
|
||||
'pt' => 'https://joinpeertube.org/ns#',
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'@type' => '@id',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return \apply_filters( 'activitypub_json_context', $context );
|
||||
}
|
||||
|
||||
function safe_remote_post( $url, $body, $user_id ) {
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$digest = \Activitypub\Signature::generate_digest( $body );
|
||||
$signature = \Activitypub\Signature::generate_signature( $user_id, 'post', $url, $date, $digest );
|
||||
|
||||
$wp_version = \get_bloginfo( 'version' );
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
$args = array(
|
||||
'timeout' => 100,
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Digest' => "SHA-256=$digest",
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
'body' => $body,
|
||||
);
|
||||
|
||||
$response = \wp_safe_remote_post( $url, $args );
|
||||
|
||||
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
function safe_remote_get( $url, $user_id ) {
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$signature = \Activitypub\Signature::generate_signature( $user_id, 'get', $url, $date );
|
||||
|
||||
$wp_version = \get_bloginfo( 'version' );
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
$args = array(
|
||||
'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
);
|
||||
|
||||
$response = \wp_safe_remote_get( $url, $args );
|
||||
|
||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url, $user_id );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a users WebFinger "resource"
|
||||
*
|
||||
* @param int $user_id
|
||||
*
|
||||
* @return string The user-resource
|
||||
*/
|
||||
function get_webfinger_resource( $user_id ) {
|
||||
return \Activitypub\Webfinger::get_user_resource( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* [get_metadata_by_actor description]
|
||||
*
|
||||
* @param string $actor
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function get_remote_metadata_by_actor( $actor ) {
|
||||
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
|
||||
if ( $pre ) {
|
||||
return $pre;
|
||||
}
|
||||
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
|
||||
$actor = Webfinger::resolve( $actor );
|
||||
}
|
||||
|
||||
if ( ! $actor ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$transient_key = 'activitypub_' . $actor;
|
||||
$metadata = \get_transient( $transient_key );
|
||||
|
||||
if ( $metadata ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( ! \wp_http_validate_url( $actor ) ) {
|
||||
$metadata = new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $actor );
|
||||
\set_transient( $transient_key, $metadata, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$user = \get_users(
|
||||
array(
|
||||
'number' => 1,
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
// we just need any user to generate a request signature
|
||||
$user_id = \reset( $user );
|
||||
$short_timeout = function() {
|
||||
return 3;
|
||||
};
|
||||
add_filter( 'activitypub_remote_get_timeout', $short_timeout );
|
||||
$response = \Activitypub\safe_remote_get( $actor, $user_id );
|
||||
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 );
|
||||
|
||||
if ( ! $metadata ) {
|
||||
$metadata = new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* [get_inbox_by_actor description]
|
||||
* @param [type] $actor [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
function get_inbox_by_actor( $actor ) {
|
||||
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
||||
|
||||
if ( \is_wp_error( $metadata ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
||||
return $metadata['endpoints']['sharedInbox'];
|
||||
}
|
||||
|
||||
if ( \array_key_exists( 'inbox', $metadata ) ) {
|
||||
return $metadata['inbox'];
|
||||
}
|
||||
|
||||
return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata );
|
||||
}
|
||||
|
||||
/**
|
||||
* [get_inbox_by_actor description]
|
||||
* @param [type] $actor [description]
|
||||
* @return [type] [description]
|
||||
*/
|
||||
function get_publickey_by_actor( $actor, $key_id ) {
|
||||
$metadata = \Activitypub\get_remote_metadata_by_actor( $actor );
|
||||
|
||||
if ( \is_wp_error( $metadata ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $metadata['publicKey'] ) &&
|
||||
isset( $metadata['publicKey']['id'] ) &&
|
||||
isset( $metadata['publicKey']['owner'] ) &&
|
||||
isset( $metadata['publicKey']['publicKeyPem'] ) &&
|
||||
$key_id === $metadata['publicKey']['id'] &&
|
||||
$actor === $metadata['publicKey']['owner']
|
||||
) {
|
||||
return $metadata['publicKey']['publicKeyPem'];
|
||||
}
|
||||
|
||||
return new \WP_Error( 'activitypub_no_public_key', \__( 'No "Public-Key" found', 'activitypub' ), $metadata );
|
||||
}
|
||||
|
||||
function get_follower_inboxes( $user_id ) {
|
||||
$followers = \Activitypub\Peer\Followers::get_followers( $user_id );
|
||||
$inboxes = array();
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$inbox = \Activitypub\get_inbox_by_actor( $follower );
|
||||
if ( ! $inbox || \is_wp_error( $inbox ) ) {
|
||||
continue;
|
||||
}
|
||||
// init array if empty
|
||||
if ( ! isset( $inboxes[ $inbox ] ) ) {
|
||||
$inboxes[ $inbox ] = array();
|
||||
}
|
||||
$inboxes[ $inbox ][] = $follower;
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
function get_identifier_settings( $user_id ) {
|
||||
?>
|
||||
<table class="form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label><?php \esc_html_e( 'Profile identifier', 'activitypub' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<p><code><?php echo \esc_html( \Activitypub\get_webfinger_resource( $user_id ) ); ?></code> or <code><?php echo \esc_url( \get_author_posts_url( $user_id ) ); ?></code></p>
|
||||
<?php // translators: the webfinger resource ?>
|
||||
<p class="description"><?php \printf( \esc_html__( 'Try to follow "@%s" by searching for it on Mastodon,Friendica & Co.', 'activitypub' ), \esc_html( \Activitypub\get_webfinger_resource( $user_id ) ) ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
function get_followers( $user_id ) {
|
||||
$followers = \Activitypub\Peer\Followers::get_followers( $user_id );
|
||||
|
||||
if ( ! $followers ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $followers;
|
||||
}
|
||||
|
||||
function count_followers( $user_id ) {
|
||||
$followers = \Activitypub\get_followers( $user_id );
|
||||
|
||||
return \count( $followers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a url and try to determine the author ID it represents.
|
||||
*
|
||||
* Checks are supposedly from the hosted site blog.
|
||||
*
|
||||
* @param string $url Permalink to check.
|
||||
*
|
||||
* @return int User ID, or 0 on failure.
|
||||
*/
|
||||
function url_to_authorid( $url ) {
|
||||
global $wp_rewrite;
|
||||
|
||||
// check if url hase the same host
|
||||
if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first, check to see if there is a 'author=N' to match against
|
||||
if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
|
||||
$id = \absint( $values[1] );
|
||||
if ( $id ) {
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if we are using rewrite rules
|
||||
$rewrite = $wp_rewrite->wp_rewrite_rules();
|
||||
|
||||
// not using rewrite rules, and 'author=N' method failed, so we're out of options
|
||||
if ( empty( $rewrite ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// generate rewrite rule for the author url
|
||||
$author_rewrite = $wp_rewrite->get_author_permastruct();
|
||||
$author_regexp = \str_replace( '%author%', '', $author_rewrite );
|
||||
|
||||
// match the rewrite rule with the passed url
|
||||
if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
|
||||
$user = \get_user_by( 'slug', $match[2] );
|
||||
if ( $user ) {
|
||||
return $user->ID;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'template-tags',
|
||||
'title' => \__( 'Template Tags', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p>' . __( 'The following Template Tags are available:', 'activitypub' ) . '</p>' .
|
||||
'<dl>' .
|
||||
'<dt><code>[ap_title]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_excerpt lenght="400"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). <code>length</code> attribute is optional.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_permalink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an <a /> tag). <code>type</code> attribute is optional.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_shortlink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s shortlink. <code>type</code> can be either <code>url</code> or <code>html</code> (an <a /> tag). I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>, to prettify the Shortlinks. <code>type</code> attribute is optional.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_hashtags]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_hashcats]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_image type=full]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: <code>thumbnail</code>, <code>medium</code>, <code>large</code>, <code>full</code>. <code>type</code> attribute is optional.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_author]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_authorurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_date]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_time]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_datetime]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_blogurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_blogname]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The name of the site.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'<dt><code>[ap_blogdesc]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The description of the site.', 'activitypub' ), 'default' ) . '</dd>' .
|
||||
'</dl>' .
|
||||
'<p>' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \wp_kses( \__( '<a href="https://github.com/pfefferle/wordpress-activitypub/issues/new" target="_blank">Let me know</a> if you miss a Template Tag.', 'activitypub' ), 'activitypub' ) . '</p>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'glossar',
|
||||
'title' => \__( 'Glossar', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p><h2>' . \__( 'Fediverse', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more informations please visit <a href="https://fediverse.party/" target="_blank">fediverse.party</a>', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'ActivityPub', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'WebFinger', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form <code>@username@domain</code>. In practical terms, <code>@user@example.com</code> is not the same as <code>@user@example.org</code>. If the domain is not included, Mastodon will try to find a local user named <code>@username</code>. However, in order to deliver to someone over ActivityPub, the <code>@username@domain</code> mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the <a href="https://docs.joinmastodon.org/spec/webfinger/" target="_blank">Mastodon Documentation</a>)', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more informations please visit <a href="https://webfinger.net/" target="_blank">webfinger.net</a>', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'NodeInfo', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( '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>' . \__( 'For more informations please visit <a href="http://nodeinfo.diaspora.software/" target="_blank">nodeinfo.diaspora.software</a>', 'activitypub' ) . '</p>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->set_help_sidebar(
|
||||
'<p><strong>' . \__( 'For more information:', 'activitypub' ) . '</strong></p>' .
|
||||
'<p>' . \__( '<a href="https://wordpress.org/support/plugin/activitypub/">Get support</a>', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( '<a href="https://github.com/pfefferle/wordpress-activitypub/issues">Report an issue</a>', 'activitypub' ) . '</p>' .
|
||||
'<hr />' .
|
||||
'<p>' . \__( '<a href="https://notiz.blog/donate">Donate</a>', 'activitypub' ) . '</p>'
|
||||
);
|
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
namespace Activitypub\Model;
|
||||
|
||||
/**
|
||||
* ActivityPub Post Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Activity {
|
||||
private $context = array( 'https://www.w3.org/ns/activitystreams' );
|
||||
private $published = '';
|
||||
private $id = '';
|
||||
private $type = 'Create';
|
||||
private $actor = '';
|
||||
private $to = array( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||
private $cc = array( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||
private $object = null;
|
||||
|
||||
const TYPE_SIMPLE = 'simple';
|
||||
const TYPE_FULL = 'full';
|
||||
const TYPE_NONE = 'none';
|
||||
|
||||
public function __construct( $type = 'Create', $context = self::TYPE_SIMPLE ) {
|
||||
if ( 'none' === $context ) {
|
||||
$this->context = null;
|
||||
} elseif ( 'full' === $context ) {
|
||||
$this->context = \Activitypub\get_context();
|
||||
}
|
||||
|
||||
$this->type = \ucfirst( $type );
|
||||
$this->published = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( 'now' ) );
|
||||
}
|
||||
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
$this->$var = $params[0];
|
||||
}
|
||||
}
|
||||
|
||||
public function from_post( Post $post ) {
|
||||
$this->object = $post->to_array();
|
||||
|
||||
if ( isset( $object['published'] ) ) {
|
||||
$this->published = $object['published'];
|
||||
}
|
||||
$this->cc = array( \get_rest_url( null, '/activitypub/1.0/users/' . intval( $post->get_post_author() ) . '/followers' ) );
|
||||
|
||||
if ( isset( $this->object['attributedTo'] ) ) {
|
||||
$this->actor = $this->object['attributedTo'];
|
||||
}
|
||||
|
||||
foreach ( $post->get_tags() as $tag ) {
|
||||
if ( 'Mention' === $tag['type'] ) {
|
||||
$this->cc[] = $tag['href'];
|
||||
}
|
||||
}
|
||||
|
||||
$type = \strtolower( $this->type );
|
||||
|
||||
if ( isset( $this->object['id'] ) ) {
|
||||
$this->id = add_query_arg( 'activity', $type, $this->object['id'] );
|
||||
}
|
||||
}
|
||||
|
||||
public function from_comment( $object ) {
|
||||
|
||||
}
|
||||
|
||||
public function to_comment() {
|
||||
|
||||
}
|
||||
|
||||
public function from_remote_array( $array ) {
|
||||
|
||||
}
|
||||
|
||||
public function to_array() {
|
||||
$array = array_filter( \get_object_vars( $this ) );
|
||||
|
||||
if ( $this->context ) {
|
||||
$array = array( '@context' => $this->context ) + $array;
|
||||
}
|
||||
|
||||
unset( $array['context'] );
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to JSON
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function to_json() {
|
||||
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
|
||||
}
|
||||
|
||||
public function to_simple_array() {
|
||||
$activity = array(
|
||||
'@context' => $this->context,
|
||||
'type' => $this->type,
|
||||
'actor' => $this->actor,
|
||||
'object' => $this->object,
|
||||
'to' => $this->to,
|
||||
'cc' => $this->cc,
|
||||
);
|
||||
|
||||
if ( $this->id ) {
|
||||
$activity['id'] = $this->id;
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
public function to_simple_json() {
|
||||
return \wp_json_encode( $this->to_simple_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
|
||||
}
|
||||
}
|
@ -0,0 +1,477 @@
|
||||
<?php
|
||||
namespace Activitypub\Model;
|
||||
|
||||
/**
|
||||
* ActivityPub Post Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Post {
|
||||
/**
|
||||
* The WordPress Post Object.
|
||||
*
|
||||
* @var WP_Post
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* The Post Author.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $post_author;
|
||||
|
||||
/**
|
||||
* The Object ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The Object Summary.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $summary;
|
||||
|
||||
/**
|
||||
* The Object Summary
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* The Object Attachments. This is usually a list of Images.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $attachments;
|
||||
|
||||
/**
|
||||
* The Object Tags. This is usually the list of used Hashtags.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
/**
|
||||
* The Onject Type
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $object_type;
|
||||
|
||||
/**
|
||||
* The Allowed Tags, used in the content.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $allowed_tags = array(
|
||||
'a' => array(
|
||||
'href' => array(),
|
||||
'title' => array(),
|
||||
'class' => array(),
|
||||
'rel' => array(),
|
||||
),
|
||||
'br' => array(),
|
||||
'p' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'span' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'div' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'ul' => array(),
|
||||
'ol' => array(),
|
||||
'li' => array(),
|
||||
'strong' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'b' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'i' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'em' => array(
|
||||
'class' => array(),
|
||||
),
|
||||
'blockquote' => array(),
|
||||
'cite' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param WP_Post $post
|
||||
*/
|
||||
public function __construct( $post ) {
|
||||
$this->post = \get_post( $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function to implement getter and setter
|
||||
*
|
||||
* @param string $method
|
||||
* @param string $params
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( empty( $this->$var ) && ! empty( $this->post->$var ) ) {
|
||||
return $this->post->$var;
|
||||
}
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
$this->$var = $params[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this Object into an Array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() {
|
||||
$post = $this->post;
|
||||
|
||||
$array = array(
|
||||
'id' => $this->get_id(),
|
||||
'type' => $this->get_object_type(),
|
||||
'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ),
|
||||
'attributedTo' => \get_author_posts_url( $post->post_author ),
|
||||
'summary' => $this->get_summary(),
|
||||
'inReplyTo' => null,
|
||||
'content' => $this->get_content(),
|
||||
'contentMap' => array(
|
||||
\strstr( \get_locale(), '_', true ) => $this->get_content(),
|
||||
),
|
||||
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
|
||||
'attachment' => $this->get_attachments(),
|
||||
'tag' => $this->get_tags(),
|
||||
);
|
||||
|
||||
return \apply_filters( 'activitypub_post', $array, $this->post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this Object into a JSON String
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function to_json() {
|
||||
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of an Activity Object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_id() {
|
||||
if ( $this->id ) {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
$post = $this->post;
|
||||
|
||||
if ( 'trash' === get_post_status( $post ) ) {
|
||||
$permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
|
||||
} else {
|
||||
$permalink = \get_permalink( $post );
|
||||
}
|
||||
|
||||
$this->id = $permalink;
|
||||
|
||||
return $permalink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Image Attachments
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_attachments() {
|
||||
if ( $this->attachments ) {
|
||||
return $this->attachments;
|
||||
}
|
||||
|
||||
$max_images = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
|
||||
|
||||
$images = array();
|
||||
|
||||
// max images can't be negative or zero
|
||||
if ( $max_images <= 0 ) {
|
||||
return $images;
|
||||
}
|
||||
|
||||
$id = $this->post->ID;
|
||||
|
||||
$image_ids = array();
|
||||
|
||||
// list post thumbnail first if this post has one
|
||||
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
||||
$image_ids[] = \get_post_thumbnail_id( $id );
|
||||
$max_images--;
|
||||
}
|
||||
|
||||
if ( $max_images > 0 ) {
|
||||
// then list any image attachments
|
||||
$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 ) {
|
||||
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
||||
$thumbnail = \wp_get_attachment_image_src( $id, 'full' );
|
||||
$mimetype = \get_post_mime_type( $id );
|
||||
|
||||
if ( $thumbnail ) {
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => $thumbnail[0],
|
||||
'mediaType' => $mimetype,
|
||||
);
|
||||
if ( $alt ) {
|
||||
$image['name'] = $alt;
|
||||
}
|
||||
$images[] = $image;
|
||||
}
|
||||
}
|
||||
|
||||
$this->attachments = $images;
|
||||
|
||||
return $images;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Tags, used in the Post
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_tags() {
|
||||
if ( $this->tags ) {
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
$tags = array();
|
||||
|
||||
$post_tags = \get_the_tags( $this->post->ID );
|
||||
if ( $post_tags ) {
|
||||
foreach ( $post_tags as $post_tag ) {
|
||||
$tag = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \get_tag_link( $post_tag->term_id ),
|
||||
'name' => '#' . $post_tag->slug,
|
||||
);
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
$mentions = apply_filters( 'activitypub_extract_mentions', array(), $this->post->post_content, $this );
|
||||
if ( $mentions ) {
|
||||
foreach ( $mentions as $mention => $url ) {
|
||||
$tag = array(
|
||||
'type' => 'Mention',
|
||||
'href' => $url,
|
||||
'name' => $mention,
|
||||
);
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
$this->tags = $tags;
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the as2 object-type for a given post
|
||||
*
|
||||
* @return string the object-type
|
||||
*/
|
||||
public function get_object_type() {
|
||||
if ( $this->object_type ) {
|
||||
return $this->object_type;
|
||||
}
|
||||
|
||||
if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
|
||||
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
|
||||
}
|
||||
|
||||
$post_type = \get_post_type( $this->post );
|
||||
switch ( $post_type ) {
|
||||
case 'post':
|
||||
$post_format = \get_post_format( $this->post );
|
||||
switch ( $post_format ) {
|
||||
case 'aside':
|
||||
case 'status':
|
||||
case 'quote':
|
||||
case 'note':
|
||||
$object_type = 'Note';
|
||||
break;
|
||||
case 'gallery':
|
||||
case 'image':
|
||||
$object_type = 'Image';
|
||||
break;
|
||||
case 'video':
|
||||
$object_type = 'Video';
|
||||
break;
|
||||
case 'audio':
|
||||
$object_type = 'Audio';
|
||||
break;
|
||||
default:
|
||||
$object_type = 'Article';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'page':
|
||||
$object_type = 'Page';
|
||||
break;
|
||||
case 'attachment':
|
||||
$mime_type = \get_post_mime_type();
|
||||
$media_type = \preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type );
|
||||
switch ( $media_type ) {
|
||||
case 'audio':
|
||||
$object_type = 'Audio';
|
||||
break;
|
||||
case 'video':
|
||||
$object_type = 'Video';
|
||||
break;
|
||||
case 'image':
|
||||
$object_type = 'Image';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$object_type = 'Article';
|
||||
break;
|
||||
}
|
||||
|
||||
$this->object_type = $object_type;
|
||||
|
||||
return $object_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content for the ActivityPub Item.
|
||||
*
|
||||
* @return string the content
|
||||
*/
|
||||
public function get_content() {
|
||||
global $post;
|
||||
|
||||
if ( $this->content ) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$post = $this->post;
|
||||
$content = $this->get_post_content_template();
|
||||
|
||||
// Fill in the shortcodes.
|
||||
setup_postdata( $post );
|
||||
$content = do_shortcode( $content );
|
||||
wp_reset_postdata();
|
||||
|
||||
$content = \wpautop( \wp_kses( $content, $this->allowed_tags ) );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
|
||||
$content = \apply_filters( 'activitypub_the_content', $content, $post );
|
||||
$content = \html_entity_decode( $content, \ENT_QUOTES, 'UTF-8' );
|
||||
|
||||
$this->content = $content;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template to use to generate the content of the activitypub item.
|
||||
*
|
||||
* @return string the template
|
||||
*/
|
||||
public function get_post_content_template() {
|
||||
if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
||||
return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
||||
}
|
||||
|
||||
if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
||||
return "[ap_title]\n\n[ap_permalink type=\"html\"]";
|
||||
}
|
||||
|
||||
if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
||||
return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink type=\"html\"]";
|
||||
}
|
||||
|
||||
// Upgrade from old template codes to shortcodes.
|
||||
$content = self::upgrade_post_content_template();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
||||
*
|
||||
* @return string the updated template content
|
||||
*/
|
||||
public static function upgrade_post_content_template() {
|
||||
// Get the custom template.
|
||||
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||
|
||||
// If the old content exists but is a blank string, we're going to need a flag to updated it even
|
||||
// after setting it to the default contents.
|
||||
$need_update = false;
|
||||
|
||||
// If the old contents is blank, use the defaults.
|
||||
if ( '' === $old_content ) {
|
||||
$old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
||||
$need_update = true;
|
||||
}
|
||||
|
||||
// Set the new content to be the old content.
|
||||
$content = $old_content;
|
||||
|
||||
// Convert old templates to shortcodes.
|
||||
$content = \str_replace( '%title%', '[ap_title]', $content );
|
||||
$content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
|
||||
$content = \str_replace( '%content%', '[ap_content]', $content );
|
||||
$content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
|
||||
$content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
|
||||
$content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
|
||||
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
|
||||
|
||||
// Store the new template if required.
|
||||
if ( $content !== $old_content || $need_update ) {
|
||||
\update_option( 'activitypub_custom_post_content', $content );
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace Activitypub\Peer;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers DB-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Followers {
|
||||
|
||||
public static function get_followers( $author_id ) {
|
||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
||||
|
||||
if ( ! $followers ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
foreach ( $followers as $key => $follower ) {
|
||||
if (
|
||||
\is_array( $follower ) &&
|
||||
isset( $follower['type'] ) &&
|
||||
'Person' === $follower['type'] &&
|
||||
isset( $follower['id'] ) &&
|
||||
false !== \filter_var( $follower['id'], \FILTER_VALIDATE_URL )
|
||||
) {
|
||||
$followers[ $key ] = $follower['id'];
|
||||
}
|
||||
}
|
||||
|
||||
return $followers;
|
||||
}
|
||||
|
||||
public static function count_followers( $author_id ) {
|
||||
$followers = self::get_followers( $author_id );
|
||||
|
||||
return \count( $followers );
|
||||
}
|
||||
|
||||
public static function add_follower( $actor, $author_id ) {
|
||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
||||
|
||||
if ( ! \is_string( $actor ) ) {
|
||||
if (
|
||||
\is_array( $actor ) &&
|
||||
isset( $actor['type'] ) &&
|
||||
'Person' === $actor['type'] &&
|
||||
isset( $actor['id'] ) &&
|
||||
false !== \filter_var( $actor['id'], \FILTER_VALIDATE_URL )
|
||||
) {
|
||||
$actor = $actor['id'];
|
||||
}
|
||||
|
||||
return new \WP_Error(
|
||||
'invalid_actor_object',
|
||||
\__( 'Unknown Actor schema', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! \is_array( $followers ) ) {
|
||||
$followers = array( $actor );
|
||||
} else {
|
||||
$followers[] = $actor;
|
||||
}
|
||||
|
||||
$followers = \array_unique( $followers );
|
||||
|
||||
\update_user_meta( $author_id, 'activitypub_followers', $followers );
|
||||
}
|
||||
|
||||
public static function remove_follower( $actor, $author_id ) {
|
||||
$followers = \get_user_option( 'activitypub_followers', $author_id );
|
||||
|
||||
foreach ( $followers as $key => $value ) {
|
||||
if ( $value === $actor ) {
|
||||
unset( $followers[ $key ] );
|
||||
}
|
||||
}
|
||||
|
||||
\update_user_meta( $author_id, 'activitypub_followers', $followers );
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Activitypub\Peer;
|
||||
|
||||
/**
|
||||
* ActivityPub Users DB-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Users {
|
||||
|
||||
/**
|
||||
* Undocumented function
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function get_user_by_various( $data ) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a url and try to determine the author ID it represents.
|
||||
*
|
||||
* Checks are supposedly from the hosted site blog.
|
||||
*
|
||||
* @param string $url Permalink to check.
|
||||
*
|
||||
* @return int User ID, or 0 on failure.
|
||||
*/
|
||||
public static function url_to_authorid( $url ) {
|
||||
global $wp_rewrite;
|
||||
|
||||
// check if url hase the same host
|
||||
if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// first, check to see if there is a 'author=N' to match against
|
||||
if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
|
||||
$id = \absint( $values[1] );
|
||||
if ( $id ) {
|
||||
return $id;
|
||||
}
|
||||
}
|
||||
|
||||
// check to see if we are using rewrite rules
|
||||
$rewrite = $wp_rewrite->wp_rewrite_rules();
|
||||
|
||||
// not using rewrite rules, and 'author=N' method failed, so we're out of options
|
||||
if ( empty( $rewrite ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// generate rewrite rule for the author url
|
||||
$author_rewrite = $wp_rewrite->get_author_permastruct();
|
||||
$author_regexp = \str_replace( '%author%', '', $author_rewrite );
|
||||
|
||||
// match the rewrite rule with the passed url
|
||||
if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
|
||||
$user = \get_user_by( 'slug', $match[2] );
|
||||
if ( $user ) {
|
||||
return $user->ID;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Followers {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Followers', 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/users/(?P<user_id>\d+)/followers',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Followers', 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = \get_user_by( 'ID', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return new \WP_Error(
|
||||
'rest_invalid_param',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'params' => array(
|
||||
'user_id' => \__( 'User not found', 'activitypub' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$json->{'@context'} = \Activitypub\get_context();
|
||||
|
||||
$json->id = \home_url( \add_query_arg( null, null ) );
|
||||
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
||||
$json->actor = \get_author_posts_url( $user_id );
|
||||
$json->type = 'OrderedCollectionPage';
|
||||
|
||||
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" ); // phpcs:ignore
|
||||
$json->totalItems = \Activitypub\count_followers( $user_id ); // phpcs:ignore
|
||||
$json->orderedItems = \Activitypub\Peer\Followers::get_followers( $user_id ); // phpcs:ignore
|
||||
|
||||
$json->first = $json->partOf; // phpcs:ignore
|
||||
|
||||
$json->first = \get_rest_url( null, "/activitypub/1.0/users/$user_id/followers" );
|
||||
|
||||
$response = new \WP_REST_Response( $json, 200 );
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function( $param, $request, $key ) {
|
||||
return user_can( $param, 'publish_posts' );
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub Following REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#following
|
||||
*/
|
||||
class Following {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Following', 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/users/(?P<user_id>\d+)/following',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Following', 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = \get_user_by( 'ID', $user_id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return new \WP_Error(
|
||||
'rest_invalid_param',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'params' => array(
|
||||
'user_id' => \__( 'User not found', 'activitypub' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$json->{'@context'} = \Activitypub\get_context();
|
||||
|
||||
$json->id = \home_url( \add_query_arg( null, null ) );
|
||||
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
||||
$json->actor = \get_author_posts_url( $user_id );
|
||||
$json->type = 'OrderedCollectionPage';
|
||||
|
||||
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/following" ); // phpcs:ignore
|
||||
$json->totalItems = 0; // phpcs:ignore
|
||||
$json->orderedItems = apply_filters( 'activitypub_following', array(), $user ); // phpcs:ignore
|
||||
|
||||
$json->first = $json->partOf; // phpcs:ignore
|
||||
|
||||
$response = new \WP_REST_Response( $json, 200 );
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function( $param, $request, $key ) {
|
||||
return user_can( $param, 'publish_posts' );
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,564 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub Inbox REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*/
|
||||
class Inbox {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Inbox', 'register_routes' ) );
|
||||
\add_filter( 'rest_pre_serve_request', array( '\Activitypub\Rest\Inbox', 'serve_request' ), 11, 4 );
|
||||
\add_action( 'activitypub_inbox_follow', array( '\Activitypub\Rest\Inbox', 'handle_follow' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_undo', array( '\Activitypub\Rest\Inbox', 'handle_unfollow' ), 10, 2 );
|
||||
//\add_action( 'activitypub_inbox_like', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 );
|
||||
//\add_action( 'activitypub_inbox_announce', array( '\Activitypub\Rest\Inbox', 'handle_reaction' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_create', array( '\Activitypub\Rest\Inbox', 'handle_create' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/inbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ),
|
||||
'args' => self::shared_inbox_post_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/users/(?P<user_id>\d+)/inbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ),
|
||||
'args' => self::user_inbox_post_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ),
|
||||
'args' => self::user_inbox_get_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hooks into the REST API request to verify the signature.
|
||||
*
|
||||
* @param bool $served Whether the request has already been served.
|
||||
* @param WP_HTTP_ResponseInterface $result Result to send to the client. Usually a WP_REST_Response.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
* @param WP_REST_Server $server Server instance.
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
public static function serve_request( $served, $result, $request, $server ) {
|
||||
if ( '/activitypub' !== \substr( $request->get_route(), 0, 12 ) ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
$signature = $request->get_header( 'signature' );
|
||||
|
||||
if ( ! $signature ) {
|
||||
return $served;
|
||||
}
|
||||
|
||||
$headers = $request->get_headers();
|
||||
|
||||
// verify signature
|
||||
//\Activitypub\Signature::verify_signature( $headers, $key );
|
||||
|
||||
return $served;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user-inbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_inbox_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$page = $request->get_param( 'page', 0 );
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$json->{'@context'} = \Activitypub\get_context();
|
||||
$json->id = \home_url( \add_query_arg( null, null ) );
|
||||
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
||||
$json->type = 'OrderedCollectionPage';
|
||||
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/inbox" ); // phpcs:ignore
|
||||
|
||||
$json->totalItems = 0; // phpcs:ignore
|
||||
|
||||
$json->orderedItems = array(); // phpcs:ignore
|
||||
|
||||
$json->first = $json->partOf; // phpcs:ignore
|
||||
|
||||
// filter output
|
||||
$json = \apply_filters( 'activitypub_inbox_array', $json );
|
||||
|
||||
/*
|
||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_post' );
|
||||
|
||||
$response = new \WP_REST_Response( $json, 200 );
|
||||
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user-inbox requests
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_inbox_post( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
|
||||
$data = $request->get_params();
|
||||
$type = $request->get_param( 'type' );
|
||||
$type = \strtolower( $type );
|
||||
|
||||
\do_action( 'activitypub_inbox', $data, $user_id, $type );
|
||||
\do_action( "activitypub_inbox_{$type}", $data, $user_id );
|
||||
|
||||
return new \WP_REST_Response( array(), 202 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared inbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function shared_inbox_post( $request ) {
|
||||
$data = $request->get_params();
|
||||
$type = $request->get_param( 'type' );
|
||||
$users = self::extract_recipients( $data );
|
||||
|
||||
if ( ! $users ) {
|
||||
return new \WP_Error(
|
||||
'rest_invalid_param',
|
||||
\__( 'No recipients found', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'params' => array(
|
||||
'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
|
||||
'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
|
||||
'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ),
|
||||
'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ),
|
||||
'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$type = \strtolower( $type );
|
||||
|
||||
\do_action( 'activitypub_inbox', $data, $user->ID, $type );
|
||||
\do_action( "activitypub_inbox_{$type}", $data, $user->ID );
|
||||
}
|
||||
|
||||
return new \WP_REST_Response( array(), 202 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function user_inbox_get_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function( $param, $request, $key ) {
|
||||
return user_can( $param, 'publish_posts' );
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function user_inbox_post_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function( $param, $request, $key ) {
|
||||
return user_can( $param, 'publish_posts' );
|
||||
},
|
||||
);
|
||||
|
||||
$params['id'] = array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'esc_url_raw',
|
||||
);
|
||||
|
||||
$params['actor'] = array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => function( $param, $request, $key ) {
|
||||
if ( ! \is_string( $param ) ) {
|
||||
$param = $param['id'];
|
||||
}
|
||||
return \esc_url_raw( $param );
|
||||
},
|
||||
);
|
||||
|
||||
$params['type'] = array(
|
||||
'required' => true,
|
||||
//'type' => 'enum',
|
||||
//'enum' => array( 'Create' ),
|
||||
//'sanitize_callback' => function( $param, $request, $key ) {
|
||||
// return \strtolower( $param );
|
||||
//},
|
||||
);
|
||||
|
||||
$params['object'] = array(
|
||||
'required' => true,
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function shared_inbox_post_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'esc_url_raw',
|
||||
);
|
||||
|
||||
$params['actor'] = array(
|
||||
'required' => true,
|
||||
//'type' => array( 'object', 'string' ),
|
||||
'sanitize_callback' => function( $param, $request, $key ) {
|
||||
if ( ! \is_string( $param ) ) {
|
||||
$param = $param['id'];
|
||||
}
|
||||
return \esc_url_raw( $param );
|
||||
},
|
||||
);
|
||||
|
||||
$params['type'] = array(
|
||||
'required' => true,
|
||||
//'type' => 'enum',
|
||||
//'enum' => array( 'Create' ),
|
||||
//'sanitize_callback' => function( $param, $request, $key ) {
|
||||
// return \strtolower( $param );
|
||||
//},
|
||||
);
|
||||
|
||||
$params['object'] = array(
|
||||
'required' => true,
|
||||
//'type' => 'object',
|
||||
);
|
||||
|
||||
$params['to'] = array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => function( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
$params['cc'] = array(
|
||||
'sanitize_callback' => function( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
$params['bcc'] = array(
|
||||
'sanitize_callback' => function( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Follow" requests
|
||||
*
|
||||
* @param array $object The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*/
|
||||
public static function handle_follow( $object, $user_id ) {
|
||||
// save follower
|
||||
\Activitypub\Peer\Followers::add_follower( $object['actor'], $user_id );
|
||||
|
||||
// get inbox
|
||||
$inbox = \Activitypub\get_inbox_by_actor( $object['actor'] );
|
||||
|
||||
// send "Accept" activity
|
||||
$activity = new \Activitypub\Model\Activity( 'Accept', \Activitypub\Model\Activity::TYPE_SIMPLE );
|
||||
$activity->set_object( $object );
|
||||
$activity->set_actor( \get_author_posts_url( $user_id ) );
|
||||
$activity->set_to( $object['actor'] );
|
||||
$activity->set_id( \get_author_posts_url( $user_id ) . '#follow-' . \preg_replace( '~^https?://~', '', $object['actor'] ) );
|
||||
|
||||
$activity = $activity->to_simple_json();
|
||||
|
||||
$response = \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Unfollow" requests
|
||||
*
|
||||
* @param array $object The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*/
|
||||
public static function handle_unfollow( $object, $user_id ) {
|
||||
if ( isset( $object['object'] ) && isset( $object['object']['type'] ) && 'Follow' === $object['object']['type'] ) {
|
||||
\Activitypub\Peer\Followers::remove_follower( $object['actor'], $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Reaction" requests
|
||||
*
|
||||
* @param array $object The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*/
|
||||
public static function handle_reaction( $object, $user_id ) {
|
||||
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
|
||||
|
||||
$comment_post_id = \url_to_postid( $object['object'] );
|
||||
|
||||
// save only replys and reactions
|
||||
if ( ! $comment_post_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$commentdata = array(
|
||||
'comment_post_ID' => $comment_post_id,
|
||||
'comment_author' => \esc_attr( $meta['name'] ),
|
||||
'comment_author_email' => '',
|
||||
'comment_author_url' => \esc_url_raw( $object['actor'] ),
|
||||
'comment_content' => \esc_url_raw( $object['actor'] ),
|
||||
'comment_type' => \esc_attr( \strtolower( $object['type'] ) ),
|
||||
'comment_parent' => 0,
|
||||
'comment_meta' => array(
|
||||
'source_url' => \esc_url_raw( $object['id'] ),
|
||||
'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
|
||||
'protocol' => 'activitypub',
|
||||
),
|
||||
);
|
||||
|
||||
// disable flood control
|
||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||
|
||||
// do not require email for AP entries
|
||||
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
|
||||
$state = \wp_new_comment( $commentdata, true );
|
||||
|
||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
|
||||
// re-add flood control
|
||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Create" requests
|
||||
*
|
||||
* @param array $object The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*/
|
||||
public static function handle_create( $object, $user_id ) {
|
||||
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
|
||||
|
||||
if ( ! isset( $object['object']['inReplyTo'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if Activity is public or not
|
||||
if ( ! self::is_activity_public( $object ) ) {
|
||||
// @todo maybe send email
|
||||
return;
|
||||
}
|
||||
|
||||
$comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
|
||||
|
||||
// save only replys and reactions
|
||||
if ( ! $comment_post_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$commentdata = array(
|
||||
'comment_post_ID' => $comment_post_id,
|
||||
'comment_author' => \esc_attr( $meta['name'] ),
|
||||
'comment_author_url' => \esc_url_raw( $object['actor'] ),
|
||||
'comment_content' => \wp_filter_kses( $object['object']['content'] ),
|
||||
'comment_type' => '',
|
||||
'comment_author_email' => '',
|
||||
'comment_parent' => 0,
|
||||
'comment_meta' => array(
|
||||
'source_url' => \esc_url_raw( $object['object']['url'] ),
|
||||
'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
|
||||
'protocol' => 'activitypub',
|
||||
),
|
||||
);
|
||||
|
||||
// disable flood control
|
||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||
|
||||
// do not require email for AP entries
|
||||
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
|
||||
$state = \wp_new_comment( $commentdata, true );
|
||||
|
||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
|
||||
// re-add flood control
|
||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract recipient URLs from Activity object
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array The list of user URLs
|
||||
*/
|
||||
public static function extract_recipients( $data ) {
|
||||
$recipient_items = array();
|
||||
|
||||
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
||||
if ( array_key_exists( $i, $data ) ) {
|
||||
if ( is_array( $data[ $i ] ) ) {
|
||||
$recipient = $data[ $i ];
|
||||
} else {
|
||||
$recipient = array( $data[ $i ] );
|
||||
}
|
||||
$recipient_items = array_merge( $recipient_items, $recipient );
|
||||
}
|
||||
|
||||
if ( array_key_exists( $i, $data['object'] ) ) {
|
||||
if ( is_array( $data['object'][ $i ] ) ) {
|
||||
$recipient = $data['object'][ $i ];
|
||||
} else {
|
||||
$recipient = array( $data['object'][ $i ] );
|
||||
}
|
||||
$recipient_items = array_merge( $recipient_items, $recipient );
|
||||
}
|
||||
}
|
||||
|
||||
$recipients = array();
|
||||
|
||||
// flatten array
|
||||
foreach ( $recipient_items as $recipient ) {
|
||||
if ( is_array( $recipient ) ) {
|
||||
// check if recipient is an object
|
||||
if ( array_key_exists( 'id', $recipient ) ) {
|
||||
$recipients[] = $recipient['id'];
|
||||
}
|
||||
} else {
|
||||
$recipients[] = $recipient;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique( $recipients );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local user recipients
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array The list of local users
|
||||
*/
|
||||
public static function get_recipients( $data ) {
|
||||
$recipients = self::extract_recipients( $data );
|
||||
$users = array();
|
||||
|
||||
foreach ( $recipients as $recipient ) {
|
||||
$user_id = \Activitypub\url_to_authorid( $recipient );
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
if ( $user ) {
|
||||
$users[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Activity is Public
|
||||
*
|
||||
* @param array $data
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_activity_public( $data ) {
|
||||
$recipients = self::extract_recipients( $data );
|
||||
|
||||
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
|
||||
}
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub NodeInfo REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see http://nodeinfo.diaspora.software/
|
||||
*/
|
||||
class Nodeinfo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Nodeinfo', 'register_routes' ) );
|
||||
\add_filter( 'nodeinfo_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo_discovery' ), 10, 2 );
|
||||
\add_filter( 'nodeinfo2_data', array( '\Activitypub\Rest\Nodeinfo', 'add_nodeinfo2_discovery' ), 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/nodeinfo/discovery',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'discovery' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/nodeinfo',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/nodeinfo2',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Nodeinfo', 'nodeinfo2' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function nodeinfo( $request ) {
|
||||
$nodeinfo = array();
|
||||
|
||||
$nodeinfo['version'] = '2.0';
|
||||
$nodeinfo['software'] = array(
|
||||
'name' => 'wordpress',
|
||||
'version' => \get_bloginfo( 'version' ),
|
||||
);
|
||||
|
||||
$users = \count_users();
|
||||
$posts = \wp_count_posts();
|
||||
$comments = \wp_count_comments();
|
||||
|
||||
$nodeinfo['usage'] = array(
|
||||
'users' => array(
|
||||
'total' => (int) $users['total_users'],
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
);
|
||||
|
||||
$nodeinfo['openRegistrations'] = false;
|
||||
$nodeinfo['protocols'] = array( 'activitypub' );
|
||||
|
||||
$nodeinfo['services'] = array(
|
||||
'inbound' => array(),
|
||||
'outbound' => array(),
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $nodeinfo, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function nodeinfo2( $request ) {
|
||||
$nodeinfo = array();
|
||||
|
||||
$nodeinfo['version'] = '1.0';
|
||||
$nodeinfo['server'] = array(
|
||||
'baseUrl' => \home_url( '/' ),
|
||||
'name' => \get_bloginfo( 'name' ),
|
||||
'software' => 'wordpress',
|
||||
'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,
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
);
|
||||
|
||||
$nodeinfo['openRegistrations'] = false;
|
||||
$nodeinfo['protocols'] = array( 'activitypub' );
|
||||
|
||||
$nodeinfo['services'] = array(
|
||||
'inbound' => array(),
|
||||
'outbound' => array(),
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $nodeinfo, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo discovery file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function discovery( $request ) {
|
||||
$discovery = array();
|
||||
$discovery['links'] = array(
|
||||
array(
|
||||
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
'href' => \get_rest_url( null, 'activitypub/1.0/nodeinfo' ),
|
||||
),
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $discovery, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo data
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo data
|
||||
* @param string The NodeInfo Version
|
||||
*
|
||||
* @return array The extended array
|
||||
*/
|
||||
public static function add_nodeinfo_discovery( $nodeinfo, $version ) {
|
||||
if ( '2.0' === $version ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
} else {
|
||||
$nodeinfo['protocols']['inbound'][] = 'activitypub';
|
||||
$nodeinfo['protocols']['outbound'][] = 'activitypub';
|
||||
}
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo2 data
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo2 data
|
||||
*
|
||||
* @return array The extended array
|
||||
*/
|
||||
public static function add_nodeinfo2_discovery( $nodeinfo ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub OStatus REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/community/ostatus/
|
||||
*/
|
||||
class Ostatus {
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/ostatus/remote-follow',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Ostatus', 'get' ),
|
||||
// 'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function get() {
|
||||
// @todo implement
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub Outbox REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#outbox
|
||||
*/
|
||||
class Outbox {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Outbox', 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/users/(?P<user_id>\d+)/outbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Outbox', 'user_outbox_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user-outbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_outbox_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$author = \get_user_by( 'ID', $user_id );
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
|
||||
|
||||
if ( ! $author ) {
|
||||
return new \WP_Error(
|
||||
'rest_invalid_param',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'params' => array(
|
||||
'user_id' => \__( 'User not found', 'activitypub' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$page = $request->get_param( 'page', 0 );
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$json->{'@context'} = \Activitypub\get_context();
|
||||
$json->id = \home_url( \add_query_arg( null, null ) );
|
||||
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
||||
$json->actor = \get_author_posts_url( $user_id );
|
||||
$json->type = 'OrderedCollectionPage';
|
||||
$json->partOf = \get_rest_url( null, "/activitypub/1.0/users/$user_id/outbox" ); // phpcs:ignore
|
||||
$json->totalItems = 0; // phpcs:ignore
|
||||
|
||||
// phpcs:ignore
|
||||
$json->totalItems = 0;
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$count_posts = \wp_count_posts( $post_type );
|
||||
$json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore
|
||||
}
|
||||
|
||||
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
|
||||
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 10 ), $json->partOf ); // phpcs:ignore
|
||||
|
||||
if ( $page && ( ( \ceil ( $json->totalItems / 10 ) ) > $page ) ) { // phpcs:ignore
|
||||
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
|
||||
}
|
||||
|
||||
if ( $page ) {
|
||||
$posts = \get_posts(
|
||||
array(
|
||||
'posts_per_page' => 10,
|
||||
'author' => $user_id,
|
||||
'offset' => ( $page - 1 ) * 10,
|
||||
'post_type' => $post_types,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$activitypub_post = new \Activitypub\Model\Post( $post );
|
||||
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_NONE );
|
||||
$activitypub_activity->from_post( $activitypub_post );
|
||||
$json->orderedItems[] = $activitypub_activity->to_array(); // phpcs:ignore
|
||||
}
|
||||
}
|
||||
|
||||
// filter output
|
||||
$json = \apply_filters( 'activitypub_outbox_array', $json );
|
||||
|
||||
/*
|
||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_post' );
|
||||
|
||||
$response = new \WP_REST_Response( $json, 200 );
|
||||
|
||||
$response->header( 'Content-Type', 'application/activity+json' );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'integer',
|
||||
'validate_callback' => function( $param, $request, $key ) {
|
||||
return user_can( $param, 'publish_posts' );
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://webfinger.net/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'rest_api_init', array( '\Activitypub\Rest\Webfinger', 'register_routes' ) );
|
||||
\add_action( 'webfinger_user_data', array( '\Activitypub\Rest\Webfinger', 'add_webfinger_discovery' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
'activitypub/1.0',
|
||||
'/webfinger',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( '\Activitypub\Rest\Webfinger', 'webfinger' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render JRD file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function webfinger( $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
|
||||
if ( \strpos( $resource, '@' ) === false ) {
|
||||
return new \WP_Error( 'activitypub_unsupported_resource', \__( 'Resource is invalid', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$resource = \str_replace( 'acct:', '', $resource );
|
||||
|
||||
$resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
|
||||
$resource_host = \substr( \strrchr( $resource, '@' ), 1 );
|
||||
|
||||
if ( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) !== $resource_host ) {
|
||||
return new \WP_Error( 'activitypub_wrong_host', \__( 'Resource host does not match blog host', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$user = \get_user_by( 'login', \esc_sql( $resource_identifier ) );
|
||||
|
||||
if ( ! $user || ! user_can( $user, 'publish_posts' ) ) {
|
||||
return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$json = array(
|
||||
'subject' => $resource,
|
||||
'aliases' => array(
|
||||
\get_author_posts_url( $user->ID ),
|
||||
),
|
||||
'links' => array(
|
||||
array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => \get_author_posts_url( $user->ID ),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => \get_author_posts_url( $user->ID ),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $json, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['resource'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'pattern' => '^acct:(.+)@(.+)$',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links
|
||||
*
|
||||
* @param array $array the jrd array
|
||||
* @param string $resource the WebFinger resource
|
||||
* @param WP_User $user the WordPress user
|
||||
*/
|
||||
public static function add_webfinger_discovery( $array, $resource, $user ) {
|
||||
$array['links'][] = array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => \get_author_posts_url( $user->ID ),
|
||||
);
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace Activitypub\Table;
|
||||
|
||||
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
class Followers_List extends \WP_List_Table {
|
||||
public function get_columns() {
|
||||
return array(
|
||||
'identifier' => \__( 'Identifier', 'activitypub' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function get_sortable_columns() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function prepare_items() {
|
||||
$columns = $this->get_columns();
|
||||
$hidden = array();
|
||||
|
||||
$this->process_action();
|
||||
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
|
||||
|
||||
$this->items = array();
|
||||
|
||||
foreach ( \Activitypub\Peer\Followers::get_followers( \get_current_user_id() ) as $follower ) {
|
||||
$this->items[]['identifier'] = \esc_attr( $follower );
|
||||
}
|
||||
}
|
||||
|
||||
public function column_default( $item, $column_name ) {
|
||||
return $item[ $column_name ];
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user