Merge branch 'master' into Comments

This commit is contained in:
Django Doucet 2023-01-28 12:50:07 -07:00
commit f9c0edc681
38 changed files with 1635 additions and 1052 deletions

27
Dockerfile Normal file
View file

@ -0,0 +1,27 @@
FROM php:7.4-alpine3.13
RUN mkdir /app
WORKDIR /app
# Install Git, NPM & needed libraries
RUN apk update \
&& apk add bash git nodejs npm gettext subversion mysql mysql-client zip \
&& rm -f /var/cache/apk/*
RUN docker-php-ext-install mysqli
# Install Composer
RUN EXPECTED_CHECKSUM=$(curl -s https://composer.github.io/installer.sig) \
&& curl https://getcomposer.org/installer -o composer-setup.php \
&& ACTUAL_CHECKSUM="$(php -r "echo hash_file('sha384', 'composer-setup.php');")" \
&& if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then >&2 echo 'ERROR: Invalid installer checksum'; rm composer-setup.php; exit 1; fi \
&& php composer-setup.php --quiet \
&& php -r "unlink('composer-setup.php');" \
&& mv composer.phar /usr/local/bin/composer
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar && \
chmod +x wp-cli.phar && \
mv wp-cli.phar /usr/local/bin/wp
RUN chmod +x -R ./

View file

@ -4,7 +4,7 @@
**Tags:** OStatus, fediverse, activitypub, activitystream **Tags:** OStatus, fediverse, activitypub, activitystream
**Requires at least:** 4.7 **Requires at least:** 4.7
**Tested up to:** 6.1 **Tested up to:** 6.1
**Stable tag:** 0.14.3 **Stable tag:** 0.15.0
**Requires PHP:** 5.6 **Requires PHP:** 5.6
**License:** MIT **License:** MIT
**License URI:** http://opensource.org/licenses/MIT **License URI:** http://opensource.org/licenses/MIT
@ -88,6 +88,16 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub).
### v.next ###
* Add configuration item for number of images to attach. props [@mexon](https://github.com/mexon)
### 0.15.0 ###
* Enable ActivityPub only for users that can `publish_posts`
* Persist only public Activities
* Fix remote-delete
### 0.14.3 ### ### 0.14.3 ###
* Better error handling. props [@akirk](https://github.com/akirk) * Better error handling. props [@akirk](https://github.com/akirk)
@ -102,7 +112,7 @@ Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github
### 0.14.0 ### ### 0.14.0 ###
* Friends support: https://wordpress.org/plugins/friends/ . props [@akirk](https://github.com/akirk) * Friends support: https://wordpress.org/plugins/friends/ props [@akirk](https://github.com/akirk)
* Massive guidance improvements. props [mediaformat](https://github.com/mediaformat) & [@akirk](https://github.com/akirk) * Massive guidance improvements. props [mediaformat](https://github.com/mediaformat) & [@akirk](https://github.com/akirk)
* Add Custom Post Type support to outbox API. props [blueset](https://github.com/blueset) * Add Custom Post Type support to outbox API. props [blueset](https://github.com/blueset)
* Better hash-tag support. props [bocops](https://github.com/bocops) * Better hash-tag support. props [bocops](https://github.com/bocops)

View file

@ -19,10 +19,12 @@ namespace Activitypub;
* Initialize plugin * Initialize plugin
*/ */
function init() { function init() {
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); \defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9_-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
\defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '<strong><a><p><ul><ol><li><code><blockquote><pre><img>' ); \defined( 'ACTIVITYPUB_ALLOWED_HTML' ) || \define( 'ACTIVITYPUB_ALLOWED_HTML', '<strong><a><p><ul><ol><li><code><blockquote><pre><img>' );
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<p><strong>%title%</strong></p>\n\n%content%\n\n<p>%hashtags%</p>\n\n<p>%shortlink%</p>" ); \defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<p><strong>[ap_title]</strong></p>\n\n[ap_content]\n\n<p>[ap_hashtags]</p>\n\n<p>[ap_shortlink]</p>" );
\define( 'ACTIVITYPUB_PLUGIN', __FILE__ );
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
@ -76,6 +78,12 @@ function init() {
require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php'; require_once \dirname( __FILE__ ) . '/includes/class-hashtag.php';
\Activitypub\Hashtag::init(); \Activitypub\Hashtag::init();
require_once \dirname( __FILE__ ) . '/includes/class-shortcodes.php';
\Activitypub\Shortcodes::init();
require_once \dirname( __FILE__ ) . '/includes/class-mention.php';
\Activitypub\Mention::init();
require_once \dirname( __FILE__ ) . '/includes/class-debug.php'; require_once \dirname( __FILE__ ) . '/includes/class-debug.php';
\Activitypub\Debug::init(); \Activitypub\Debug::init();
@ -122,6 +130,8 @@ function add_rewrite_rules() {
\add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); \add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' );
\add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); \add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' );
} }
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
} }
\add_action( 'init', '\Activitypub\add_rewrite_rules', 1 ); \add_action( 'init', '\Activitypub\add_rewrite_rules', 1 );
@ -156,11 +166,3 @@ function enable_buddypress_features() {
\Activitypub\Integration\Buddypress::init(); \Activitypub\Integration\Buddypress::init();
} }
add_action( 'bp_include', '\Activitypub\enable_buddypress_features' ); add_action( 'bp_include', '\Activitypub\enable_buddypress_features' );
add_action(
'friends_load_parsers',
function( \Friends\Feed $friends_feed ) {
require_once __DIR__ . '/integration/class-friends-feed-parser-activitypub.php';
$friends_feed->register_parser( Friends_Feed_Parser_ActivityPub::SLUG, new Friends_Feed_Parser_ActivityPub( $friends_feed ) );
}
);

View file

@ -1,6 +1,7 @@
.settings_page_activitypub .notice { .settings_page_activitypub .notice {
max-width: 800px; max-width: 800px;
margin: 0 auto; margin: auto;
margin-top: 10px;
} }
.activitypub-settings-header { .activitypub-settings-header {

View file

@ -34,7 +34,7 @@
"scripts": { "scripts": {
"test": [ "test": [
"composer install", "composer install",
"bin/install-wp-tests.sh wordpress wordpress wordpress", "bin/install-wp-tests.sh activitypub-test root activitypub-test test-db latest true",
"vendor/bin/phpunit" "vendor/bin/phpunit"
] ]
} }

17
docker-compose-test.yml Normal file
View file

@ -0,0 +1,17 @@
version: '2'
services:
test-db:
image: mysql:5.7
environment:
MYSQL_DATABASE: activitypub-test
MYSQL_ROOT_PASSWORD: activitypub-test
test-php:
build:
context: .
dockerfile: Dockerfile
links:
- test-db
volumes:
- .:/app
command: ["composer", "run-script", "test"]

View file

@ -30,16 +30,35 @@ class Activity_Dispatcher {
* *
* @param \Activitypub\Model\Post $activitypub_post * @param \Activitypub\Model\Post $activitypub_post
*/ */
public static function send_post_activity( $activitypub_post ) { public static function send_post_activity( Model\Post $activitypub_post ) {
// get latest version of post // get latest version of post
$user_id = $activitypub_post->get_post_author(); $user_id = $activitypub_post->get_post_author();
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL ); $activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
$activitypub_activity->from_post( $activitypub_post->to_array() ); $activitypub_activity->from_post( $activitypub_post );
foreach ( \Activitypub\get_follower_inboxes( $user_id ) as $inbox => $to ) { $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 ); $activitypub_activity->set_to( $to );
$activity = $activitypub_activity->to_json(); // phpcs:ignore $activity = $activitypub_activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id ); \Activitypub\safe_remote_post( $inbox, $activity, $user_id );
} }

View file

@ -13,7 +13,6 @@ class Activitypub {
public static function init() { public static function init() {
\add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 ); \add_filter( 'template_include', array( '\Activitypub\Activitypub', 'render_json_template' ), 99 );
\add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) ); \add_filter( 'query_vars', array( '\Activitypub\Activitypub', 'add_query_vars' ) );
\add_action( 'init', array( '\Activitypub\Activitypub', 'add_rewrite_endpoint' ) );
\add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 ); \add_filter( 'pre_get_avatar_data', array( '\Activitypub\Activitypub', 'pre_get_avatar_data' ), 11, 2 );
// Add support for ActivityPub to custom post types // Add support for ActivityPub to custom post types
@ -23,8 +22,9 @@ class Activitypub {
\add_post_type_support( $post_type, 'activitypub' ); \add_post_type_support( $post_type, 'activitypub' );
} }
\add_action( 'transition_post_status', array( '\Activitypub\Activitypub', 'schedule_post_activity' ), 10, 3 ); \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 );
} }
/** /**
@ -39,6 +39,11 @@ class Activitypub {
return $template; 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() ) { if ( \is_author() ) {
$json_template = \dirname( __FILE__ ) . '/../templates/author-json.php'; $json_template = \dirname( __FILE__ ) . '/../templates/author-json.php';
} elseif ( \Activitypub\is_ap_replies() ) { } elseif ( \Activitypub\is_ap_replies() ) {
@ -46,9 +51,9 @@ class Activitypub {
} elseif ( \Activitypub\is_ap_comment() ) { } elseif ( \Activitypub\is_ap_comment() ) {
$json_template = \dirname( __FILE__ ) . '/../templates/comment-json.php'; $json_template = \dirname( __FILE__ ) . '/../templates/comment-json.php';
} elseif ( \is_singular() ) { } elseif ( \is_singular() ) {
$json_template = \dirname( __FILE__ ) . '/../templates/post-json.php'; $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
} elseif ( \is_home() ) { } elseif ( \is_home() ) {
$json_template = \dirname( __FILE__ ) . '/../templates/blog-json.php'; $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
} }
global $wp_query; global $wp_query;
@ -99,13 +104,6 @@ class Activitypub {
return $vars; return $vars;
} }
/**
* Add our rewrite endpoint to permalinks and pages.
*/
public static function add_rewrite_endpoint() {
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
}
/** /**
* Schedule Activities. * Schedule Activities.
* *
@ -190,4 +188,26 @@ class Activitypub {
} }
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true ); 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' );
}
} }

View file

@ -56,6 +56,8 @@ class Admin {
switch ( $tab ) { switch ( $tab ) {
case 'settings': case 'settings':
\Activitypub\Model\Post::upgrade_post_content_template();
\load_template( \dirname( __FILE__ ) . '/../templates/settings.php' ); \load_template( \dirname( __FILE__ ) . '/../templates/settings.php' );
break; break;
case 'welcome': case 'welcome':
@ -111,6 +113,15 @@ class Admin {
'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT, '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( \register_setting(
'activitypub', 'activitypub',
'activitypub_object_type', 'activitypub_object_type',
@ -134,15 +145,6 @@ class Admin {
'default' => 0, 'default' => 0,
) )
); );
\register_setting(
'activitypub',
'activitypub_allowed_html',
array(
'type' => 'string',
'description' => \__( 'List of HTML elements that are allowed in activities.', 'activitypub' ),
'default' => ACTIVITYPUB_ALLOWED_HTML,
)
);
\register_setting( \register_setting(
'activitypub', 'activitypub',
'activitypub_support_post_types', 'activitypub_support_post_types',

View file

@ -12,8 +12,8 @@ class Hashtag {
*/ */
public static function init() { public static function init() {
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) { if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
\add_filter( 'wp_insert_post', array( '\Activitypub\Hashtag', 'insert_post' ), 99, 2 ); \add_filter( 'wp_insert_post', array( '\Activitypub\Hashtag', 'insert_post' ), 10, 2 );
\add_filter( 'the_content', array( '\Activitypub\Hashtag', 'the_content' ), 99, 2 ); \add_filter( 'the_content', array( '\Activitypub\Hashtag', 'the_content' ), 10, 2 );
} }
} }
@ -21,15 +21,15 @@ class Hashtag {
* Filter to save #tags as real WordPress tags * Filter to save #tags as real WordPress tags
* *
* @param int $id the rev-id * @param int $id the rev-id
* @param array $data the post-data as array * @param WP_Post $post the post
* *
* @return * @return
*/ */
public static function insert_post( $id, $data ) { public static function insert_post( $id, $post ) {
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $data->post_content, $match ) ) { if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
$tags = \implode( ', ', $match[1] ); $tags = \implode( ', ', $match[1] );
\wp_add_post_tags( $data->post_parent, $tags ); \wp_add_post_tags( $post->post_parent, $tags );
} }
return $id; return $id;
@ -43,8 +43,22 @@ class Hashtag {
* @return string the filtered post-content * @return string the filtered post-content
*/ */
public static function the_content( $the_content ) { public static function the_content( $the_content ) {
$protected_tags = array();
$the_content = preg_replace_callback(
'#<[^>]+>#i',
function( $m ) use ( &$protected_tags ) {
$c = count( $protected_tags );
$protect = '!#!#PROTECT' . $c . '#!#!';
$protected_tags[ $protect ] = $m[0];
return $protect;
},
$the_content
);
$the_content = \preg_replace_callback( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( '\Activitypub\Hashtag', 'replace_with_links' ), $the_content ); $the_content = \preg_replace_callback( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( '\Activitypub\Hashtag', 'replace_with_links' ), $the_content );
$the_content = str_replace( array_keys( $protected_tags ), array_values( $protected_tags ), $the_content );
return $the_content; return $the_content;
} }

View file

@ -0,0 +1,86 @@
<?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();
$the_content = preg_replace_callback(
'#<a.*?href=[^>]+>.*?</a>#i',
function( $m ) use ( &$protected_tags ) {
$c = count( $protected_tags );
$protect = '!#!#PROTECT' . $c . '#!#!';
$protected_tags[ $protect ] = $m[0];
return $protect;
},
$the_content
);
$the_content = \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( '\Activitypub\Mention', 'replace_with_links' ), $the_content );
$the_content = str_replace( array_keys( $protected_tags ), 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;
}
}

View file

@ -0,0 +1,510 @@
<?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 ) {
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 ) {
return '';
}
$content = \get_post_field( 'post_content', $post );
return \apply_filters( 'the_content', $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;
}
}

View file

@ -28,12 +28,21 @@ class Webfinger {
} }
public static function resolve( $account ) { public static function resolve( $account ) {
if ( ! preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $account, $m ) ) { if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $account, $m ) ) {
return null; return null;
} }
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $account, '@' ), 'https://' . $m[1] . '/.well-known/webfinger' ); $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 ) ) { if ( ! \wp_http_validate_url( $url ) ) {
return new \WP_Error( 'invalid_webfinger_url', null, $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 // try to access author URL
@ -42,28 +51,34 @@ class Webfinger {
array( array(
'headers' => array( 'Accept' => 'application/activity+json' ), 'headers' => array( 'Accept' => 'application/activity+json' ),
'redirection' => 0, 'redirection' => 0,
'timeout' => 2,
) )
); );
if ( \is_wp_error( $response ) ) { if ( \is_wp_error( $response ) ) {
return new \WP_Error( 'webfinger_url_not_accessible', null, $url ); $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;
} }
$response_code = \wp_remote_retrieve_response_code( $response );
$body = \wp_remote_retrieve_body( $response ); $body = \wp_remote_retrieve_body( $response );
$body = \json_decode( $body, true ); $body = \json_decode( $body, true );
if ( ! isset( $body['links'] ) ) { if ( empty( $body['links'] ) ) {
return new \WP_Error( 'webfinger_url_invalid_response', null, $url ); $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 ) { foreach ( $body['links'] as $link ) {
if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) { if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
\set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
return $link['href']; return $link['href'];
} }
} }
return new \WP_Error( 'webfinger_url_no_activity_pub', null, $body ); $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;
} }
} }

View file

@ -99,7 +99,7 @@ function safe_remote_get( $url, $user_id ) {
$wp_version = \get_bloginfo( 'version' ); $wp_version = \get_bloginfo( 'version' );
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
$args = array( $args = array(
'timeout' => 100, 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
'limit_response_size' => 1048576, 'limit_response_size' => 1048576,
'redirection' => 3, 'redirection' => 3,
'user-agent' => "$user_agent; ActivityPub", 'user-agent' => "$user_agent; ActivityPub",
@ -141,8 +141,8 @@ function get_remote_metadata_by_actor( $actor ) {
if ( $pre ) { if ( $pre ) {
return $pre; return $pre;
} }
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $actor ) ) { if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
$actor = \Activitypub\Webfinger::resolve( $actor ); $actor = Webfinger::resolve( $actor );
} }
if ( ! $actor ) { if ( ! $actor ) {
@ -153,30 +153,37 @@ function get_remote_metadata_by_actor( $actor ) {
return $actor; return $actor;
} }
$metadata = \get_transient( 'activitypub_' . $actor ); $transient_key = 'activitypub_' . $actor;
$metadata = \get_transient( $transient_key );
if ( $metadata ) { if ( $metadata ) {
return $metadata; return $metadata;
} }
if ( ! \wp_http_validate_url( $actor ) ) { if ( ! \wp_http_validate_url( $actor ) ) {
return new \WP_Error( 'activitypub_no_valid_actor_url', \__( 'The "actor" is no valid URL', 'activitypub' ), $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( $user = \get_users(
array( array(
'number' => 1, 'number' => 1,
'who' => 'authors', 'capability__in' => array( 'publish_posts' ),
'fields' => 'ID', 'fields' => 'ID',
) )
); );
// we just need any user to generate a request signature // we just need any user to generate a request signature
$user_id = \reset( $user ); $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 ); $response = \Activitypub\safe_remote_get( $actor, $user_id );
remove_filter( 'activitypub_remote_get_timeout', $short_timeout );
if ( \is_wp_error( $response ) ) { if ( \is_wp_error( $response ) ) {
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
return $response; return $response;
} }
@ -184,10 +191,12 @@ function get_remote_metadata_by_actor( $actor ) {
$metadata = \json_decode( $metadata, true ); $metadata = \json_decode( $metadata, true );
if ( ! $metadata ) { if ( ! $metadata ) {
return new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), $actor ); $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( 'activitypub_' . $actor, $metadata, WEEK_IN_SECONDS ); \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
return $metadata; return $metadata;
} }

View file

@ -2,45 +2,67 @@
\get_current_screen()->add_help_tab( \get_current_screen()->add_help_tab(
array( array(
'id' => 'fediverse', 'id' => 'template-tags',
'title' => \__( 'Fediverse', 'activitypub' ), 'title' => \__( 'Template Tags', 'activitypub' ),
'content' => 'content' =>
'<p><strong>' . \__( 'What is the Fediverse?', 'activitypub' ) . '</strong></p>' . '<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]</code></dt>' .
'<dd>' . \wp_kses( __( 'The post\'s content.', '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. Type can be either: <code>url</code> or <code>html</code> (an &lt;a /&gt; 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. Type can be either <code>url</code> or <code>html</code> (an &lt;a /&gt; 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>' . \__( '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>' . \__( '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>' . \__( '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>' .
\get_current_screen()->add_help_tab(
array(
'id' => 'activitypub',
'title' => \__( 'ActivityPub', 'activitypub' ),
'content' =>
'<p><strong>' . \__( 'What is ActivityPub?', 'activitypub' ) . '</strong></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>',
)
);
\get_current_screen()->add_help_tab(
array(
'id' => 'webfinger',
'title' => \__( 'WebFinger', 'activitypub' ),
'content' =>
'<p><strong>' . \__( 'What is WebFinger?', 'activitypub' ) . '</strong></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>' . \__( '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>' . \__( '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>' . \__( '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>' . \__( 'For more informations please visit <a href="https://webfinger.net/" target="_blank">webfinger.net</a>', 'activitypub' ) . '</p>' .
) '<p><h2>' . \__( 'NodeInfo', 'activitypub' ) . '</h2></p>' .
);
\get_current_screen()->add_help_tab(
array(
'id' => 'nodeinfo',
'title' => \__( 'NodeInfo', 'activitypub' ),
'content' =>
'<p><strong>' . \__( 'What is NodeInfo?', 'activitypub' ) . '</strong></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>' . \__( '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>', '<p>' . \__( 'For more informations please visit <a href="http://nodeinfo.diaspora.software/" target="_blank">nodeinfo.diaspora.software</a>', 'activitypub' ) . '</p>',
) )

View file

@ -46,20 +46,28 @@ class Activity {
} }
} }
public function from_post( $object ) { public function from_post( Post $post ) {
$this->object = $object; $this->object = $post->to_array();
if ( isset( $object['published'] ) ) { if ( isset( $object['published'] ) ) {
$this->published = $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( $object['attributedTo'] ) ) { if ( isset( $this->object['attributedTo'] ) ) {
$this->actor = $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 ); $type = \strtolower( $this->type );
if ( isset( $object['id'] ) ) { if ( isset( $this->object['id'] ) ) {
$this->id = add_query_arg( 'activity', $type, $object['id'] ); $this->id = add_query_arg( 'activity', $type, $this->object['id'] );
} }
} }

View file

@ -7,39 +7,114 @@ namespace Activitypub\Model;
* @author Matthias Pfefferle * @author Matthias Pfefferle
*/ */
class Post { class Post {
/**
* The WordPress Post Object.
*
* @var WP_Post
*/
private $post; private $post;
/**
* The Post Author.
*
* @var string
*/
private $post_author; private $post_author;
/**
* The Object ID.
*
* @var string
*/
private $id; private $id;
/**
* The Object Summary.
*
* @var string
*/
private $summary; private $summary;
/**
* The Object Summary
*
* @var string
*/
private $content; private $content;
/**
* The Object Attachments. This is usually a list of Images.
*
* @var array
*/
private $attachments; private $attachments;
/**
* The Object Tags. This is usually the list of used Hashtags.
*
* @var array
*/
private $tags; private $tags;
private $object_type;
/**
* The Object Type
*
* @var string
*/
private $object_type = 'Note';
/**
* 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(),
),
);
private $delete; private $delete;
private $updated; private $updated;
private $permalink;
private $replies; private $replies;
public function __construct( $post = null ) { /**
* Constructor
*
* @param WP_Post $post
*/
public function __construct( $post ) {
$this->post = \get_post( $post ); $this->post = \get_post( $post );
$this->post_author = $this->post->post_author;
$this->id = $this->generate_id();
$this->summary = $this->generate_the_title();
$this->content = $this->generate_the_content();
$this->attachments = $this->generate_attachments();
$this->tags = $this->generate_tags();
$this->object_type = $this->generate_object_type();
$this->delete = $this->get_deleted();
$this->updated = $this->generate_updated();
$this->permalink = $this->get_the_permalink();
$this->replies = $this->generate_replies();
} }
/**
* Magic function to implement getter and setter
*
* @param string $method
* @param string $params
*
* @return void
*/
public function __call( $method, $params ) { public function __call( $method, $params ) {
$var = \strtolower( \substr( $method, 4 ) ); $var = \strtolower( \substr( $method, 4 ) );
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
if ( empty( $this->$var ) && ! empty( $this->post->$var ) ) {
return $this->post->$var;
}
return $this->$var; return $this->$var;
} }
@ -48,32 +123,37 @@ class Post {
} }
} }
/**
* Converts this Object into an Array.
*
* @return array
*/
public function to_array() { public function to_array() {
$post = $this->post; $post = $this->post;
$array = array( $array = array(
'id' => $this->id, 'id' => $this->get_id(),
'type' => $this->object_type, 'type' => $this->get_object_type(),
'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ), 'published' => \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_date_gmt ) ),
'attributedTo' => \get_author_posts_url( $post->post_author ), 'attributedTo' => \get_author_posts_url( $post->post_author ),
'summary' => $this->summary, 'summary' => $this->get_summary(),
'inReplyTo' => null, 'inReplyTo' => null,
'url' => $this->permalink, 'url' => \get_permalink( $post->ID ),
'content' => $this->content, 'content' => $this->get_content(),
'contentMap' => array( 'contentMap' => array(
\strstr( \get_locale(), '_', true ) => $this->content, \strstr( \get_locale(), '_', true ) => $this->get_content(),
), ),
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ), 'cc' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
'attachment' => $this->attachments, 'attachment' => $this->get_attachments(),
'tag' => $this->tags, 'tag' => $this->get_tags(),
); );
if ( $this->replies ) { if ( $this->replies ) {
$array['replies'] = $this->replies; $array['replies'] = $this->replies;
} }
if ( $this->deleted ) { if ( $this->deleted ) {
$array['deleted'] = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_modified_gmt ) ); $array['deleted'] = \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $post->post_modified_gmt ) );
$deleted_post_slug = \get_post_meta( $post->ID, '_activitypub_permalink_compat', true ); $deleted_post_slug = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
if ( $deleted_post_slug ) { if ( $deleted_post_slug ) {
$array['id'] = $deleted_post_slug; $array['id'] = $deleted_post_slug;
} }
@ -84,60 +164,89 @@ class Post {
return \apply_filters( 'activitypub_post', $array ); return \apply_filters( 'activitypub_post', $array );
} }
/**
* Converts this Object into a JSON String
*
* @return string
*/
public function to_json() { public function to_json() {
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT ); return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
} }
public function generate_id() { /**
$post = $this->post; * Returns the ID of an Activity Object
$object_id = \add_query_arg( // *
array( * @return string
'p' => $post->ID, */
), public function get_id() {
trailingslashit( site_url() ) if ( $this->id ) {
); return $this->id;
$pretty_permalink = \get_post_meta( $post->ID, '_activitypub_permalink_compat', true );
if ( ! empty( $pretty_permalink ) ) {
$object_id = $pretty_permalink;
} }
$post = $this->post;
if ( 'trash' === get_post_status( $post ) && \get_post_meta( $post->ID, 'activitypub_canonical_url', true ) ) {
$object_id = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
} else {
$object_id = \add_query_arg( //
array(
'p' => $post->ID,
),
\trailingslashit( \site_url() )
);
}
$this->id = $object_id;
return $object_id; return $object_id;
} }
public function generate_attachments() { /**
$max_images = \apply_filters( 'activitypub_max_images', 3 ); * 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(); $images = array();
// max images can't be negative or zero // max images can't be negative or zero
if ( $max_images <= 0 ) { if ( $max_images <= 0 ) {
$max_images = 1; return $images;
} }
$id = $this->post->ID; $id = $this->post->ID;
$image_ids = array(); $image_ids = array();
// list post thumbnail first if this post has one // list post thumbnail first if this post has one
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) { if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$image_ids[] = \get_post_thumbnail_id( $id ); $image_ids[] = \get_post_thumbnail_id( $id );
$max_images--; $max_images--;
} }
// then list any image attachments
$query = new \WP_Query( if ( $max_images > 0 ) {
array( // then list any image attachments
'post_parent' => $id, $query = new \WP_Query(
'post_status' => 'inherit', array(
'post_type' => 'attachment', 'post_parent' => $id,
'post_mime_type' => 'image', 'post_status' => 'inherit',
'order' => 'ASC', 'post_type' => 'attachment',
'orderby' => 'menu_order ID', 'post_mime_type' => 'image',
'posts_per_page' => $max_images, '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; foreach ( $query->get_posts() as $attachment ) {
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
$image_ids[] = $attachment->ID;
}
} }
} }
@ -162,10 +271,21 @@ class Post {
} }
} }
$this->attachments = $images;
return $images; return $images;
} }
public function generate_tags() { /**
* Returns a list of Tags, used in the Post
*
* @return array
*/
public function get_tags() {
if ( $this->tags ) {
return $this->tags;
}
$tags = array(); $tags = array();
$post_tags = \get_the_tags( $this->post->ID ); $post_tags = \get_the_tags( $this->post->ID );
@ -180,6 +300,20 @@ class Post {
} }
} }
$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; return $tags;
} }
@ -230,12 +364,13 @@ class Post {
/** /**
* Returns the as2 object-type for a given post * Returns the as2 object-type for a given post
* *
* @param string $type the object-type
* @param Object $post the post-object
*
* @return string the object-type * @return string the object-type
*/ */
public function generate_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' ) ) { if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) ); return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
} }
@ -289,134 +424,102 @@ class Post {
break; break;
} }
$this->object_type = $object_type;
return $object_type; return $object_type;
} }
public function generate_the_content() { /**
* Returns the content for the ActivityPub Item.
*
* @return string the content
*/
public function get_content() {
if ( $this->content ) {
return $this->content;
}
$post = $this->post; $post = $this->post;
$content = $this->get_post_content_template(); $content = $this->get_post_content_template();
$content = \str_replace( '%title%', \get_the_title( $post->ID ), $content ); // Fill in the shortcodes.
$content = \str_replace( '%excerpt%', $this->get_the_post_excerpt(), $content ); setup_postdata( $post );
$content = \str_replace( '%content%', $this->get_the_post_content(), $content ); $content = do_shortcode( $content );
$content = \str_replace( '%permalink%', $this->get_the_post_link( 'permalink' ), $content ); wp_reset_postdata();
$content = \str_replace( '%shortlink%', $this->get_the_post_link( 'shortlink' ), $content );
$content = \str_replace( '%hashtags%', $this->get_the_post_hashtags(), $content );
// backwards compatibility
$content = \str_replace( '%tags%', $this->get_the_post_hashtags(), $content );
$content = \trim( \preg_replace( '/[\r\n]{2,}/', '', $content ) ); $content = \wpautop( \wp_kses( $content, $this->allowed_tags ) );
$filtered_content = \apply_filters( 'activitypub_the_content', $content, $this->post ); $filtered_content = \apply_filters( 'activitypub_the_content', $content, $post );
$decoded_content = \html_entity_decode( $filtered_content, \ENT_QUOTES, 'UTF-8' ); $decoded_content = \html_entity_decode( $filtered_content, \ENT_QUOTES, 'UTF-8' );
$allowed_html = \apply_filters( 'activitypub_allowed_html', \get_option( 'activitypub_allowed_html', ACTIVITYPUB_ALLOWED_HTML ) ); $content = \trim( \preg_replace( '/[\n\r]/', '', $content ) );
if ( $allowed_html ) { $this->content = $content;
return \strip_tags( $decoded_content, $allowed_html );
}
return $decoded_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() { public function get_post_content_template() {
if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) { if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "%excerpt%\n\n<p>%permalink%</p>"; return "[ap_excerpt]\n\n[ap_permalink]";
} }
if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) { if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "<p><strong>%title%</strong></p>\n\n<p>%permalink%</p>"; return "[ap_title]\n\n[ap_permalink]";
} }
if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) { if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
return "%content%\n\n<p>%hashtags%</p>\n\n<p>%permalink%</p>"; return "[ap_content]\n\n[ap_hashtags]\n\n[ap_permalink]";
} }
return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); // Upgrade from old template codes to shortcodes.
$content = self::upgrade_post_content_template();
return $content;
} }
/** /**
* Get the excerpt for a post for use outside of the loop. * Updates the custom template to use shortcodes instead of the deprecated templates.
* *
* @param int Optional excerpt length. * @return string the updated template content
*
* @return string The excerpt.
*/ */
public function get_the_post_excerpt( $excerpt_length = 400 ) { public static function upgrade_post_content_template() {
$post = $this->post; // Get the custom template.
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
$excerpt = \get_post_field( 'post_excerpt', $post ); // 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 ( '' === $excerpt ) { // If the old contents is blank, use the defaults.
if ( '' === $old_content ) {
$content = \get_post_field( 'post_content', $post ); $old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
$need_update = true;
// 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 );
$excerpt_length = \apply_filters( 'excerpt_length', $excerpt_length );
/** This filter is documented in wp-includes/formatting.php */
$excerpt_more = \apply_filters( 'excerpt_more', ' [...]' );
$excerpt = \wp_trim_words( $excerpt, $excerpt_length, $excerpt_more );
}
} }
return \apply_filters( 'the_excerpt', $excerpt ); // Set the new content to be the old content.
} $content = $old_content;
/** // Convert old templates to shortcodes.
* Get the content for a post for use outside of the loop. $content = \str_replace( '%title%', '[ap_title]', $content );
* $content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
* @return string The content. $content = \str_replace( '%content%', '[ap_content]', $content );
*/ $content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
public function get_the_post_content() { $content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
$post = $this->post; $content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
$content = \get_post_field( 'post_content', $post ); // Store the new template if required.
if ( $content !== $old_content || $need_update ) {
return \apply_filters( 'the_content', $content ); \update_option( 'activitypub_custom_post_content', $content );
}
/**
* Adds a backlink to the post/summary content
*
* @param string $content
* @param WP_Post $post
*
* @return string
*/
public function get_the_post_link( $type = 'permalink' ) {
$post = $this->post;
if ( 'shortlink' === $type ) {
$link = \esc_url( \wp_get_shortlink( $post->ID ) );
} elseif ( 'permalink' === $type ) {
$link = \esc_url( \get_permalink( $post->ID ) );
} else {
return '';
} }
return \sprintf( '<a href="%1$s">%1$s</a>', $link ); return $content;
}
/**
* Gets the federated permalink
*
* @return string
*/
public function get_the_permalink() {
$post = $this->post;
$link = \get_permalink( $post->ID );
return $link;
} }
/** /**
@ -427,7 +530,7 @@ class Post {
* *
* @return string * @return string
*/ */
public function get_the_post_hashtags() { public function get_the_mentions() {
$post = $this->post; $post = $this->post;
$tags = \get_the_tags( $post->ID ); $tags = \get_the_tags( $post->ID );

View file

@ -101,6 +101,9 @@ class Followers {
$params['user_id'] = array( $params['user_id'] = array(
'required' => true, 'required' => true,
'type' => 'integer', 'type' => 'integer',
'validate_callback' => function( $param, $request, $key ) {
return user_can( $param, 'publish_posts' );
},
); );
return $params; return $params;

View file

@ -99,6 +99,9 @@ class Following {
$params['user_id'] = array( $params['user_id'] = array(
'required' => true, 'required' => true,
'type' => 'integer', 'type' => 'integer',
'validate_callback' => function( $param, $request, $key ) {
return user_can( $param, 'publish_posts' );
},
); );
return $params; return $params;

View file

@ -35,7 +35,7 @@ class Inbox {
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ), 'callback' => array( '\Activitypub\Rest\Inbox', 'shared_inbox_post' ),
'args' => self::shared_inbox_request_parameters(), 'args' => self::shared_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
) )
@ -48,12 +48,13 @@ class Inbox {
array( array(
'methods' => \WP_REST_Server::EDITABLE, 'methods' => \WP_REST_Server::EDITABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ), 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_post' ),
'args' => self::user_inbox_request_parameters(), 'args' => self::user_inbox_post_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
array( array(
'methods' => \WP_REST_Server::READABLE, 'methods' => \WP_REST_Server::READABLE,
'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ), 'callback' => array( '\Activitypub\Rest\Inbox', 'user_inbox_get' ),
'args' => self::user_inbox_get_parameters(),
'permission_callback' => '__return_true', 'permission_callback' => '__return_true',
), ),
) )
@ -197,7 +198,7 @@ class Inbox {
* *
* @return array list of parameters * @return array list of parameters
*/ */
public static function user_inbox_request_parameters() { public static function user_inbox_get_parameters() {
$params = array(); $params = array();
$params['page'] = array( $params['page'] = array(
@ -207,6 +208,32 @@ class Inbox {
$params['user_id'] = array( $params['user_id'] = array(
'required' => true, 'required' => true,
'type' => 'integer', '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( $params['id'] = array(
@ -245,7 +272,7 @@ class Inbox {
* *
* @return array list of parameters * @return array list of parameters
*/ */
public static function shared_inbox_request_parameters() { public static function shared_inbox_post_parameters() {
$params = array(); $params = array();
$params['page'] = array( $params['page'] = array(
@ -410,6 +437,13 @@ class Inbox {
$comment_parent = 0; $comment_parent = 0;
$comment_parent_id = 0; $comment_parent_id = 0;
$meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] ); $meta = \Activitypub\get_remote_metadata_by_actor( $object['actor'] );
// check if Activity is public or not
if ( ! self::is_activity_public( $object ) ) {
// @todo maybe send email
return;
}
$avatar_url = null; $avatar_url = null;
$audience = \Activitypub\get_audience( $object ); $audience = \Activitypub\get_audience( $object );
@ -545,21 +579,53 @@ class Inbox {
} }
} }
/**
* Extract recipient URLs from Activity object
*
* @param array $data
*
* @return array The list of user URLs
*/
public static function extract_recipients( $data ) { public static function extract_recipients( $data ) {
$recipients = array(); $recipient_items = array();
$users = array();
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
if ( array_key_exists( $i, $data ) ) { if ( array_key_exists( $i, $data ) ) {
$recipients = array_merge( $recipients, $data[ $i ] ); $recipient_items = array_merge( $recipient_items, $data[ $i ] );
} }
if ( array_key_exists( $i, $data['object'] ) ) { if ( array_key_exists( $i, $data['object'] ) ) {
$recipients = array_merge( $recipients, $data[ $i ] ); $recipient_items = array_merge( $recipient_items, $data[ $i ] );
} }
} }
$recipients = array_unique( $recipients ); $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 ) { foreach ( $recipients as $recipient ) {
$user_id = \Activitypub\url_to_authorid( $recipient ); $user_id = \Activitypub\url_to_authorid( $recipient );
@ -573,4 +639,16 @@ class Inbox {
return $users; 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 );
}
} }

View file

@ -138,6 +138,9 @@ class Outbox {
$params['user_id'] = array( $params['user_id'] = array(
'required' => true, 'required' => true,
'type' => 'integer', 'type' => 'integer',
'validate_callback' => function( $param, $request, $key ) {
return user_can( $param, 'publish_posts' );
},
); );
return $params; return $params;

View file

@ -61,7 +61,7 @@ class Webfinger {
$user = \get_user_by( 'login', \esc_sql( $resource_identifier ) ); $user = \get_user_by( 'login', \esc_sql( $resource_identifier ) );
if ( ! $user ) { if ( ! $user || ! user_can( $user, 'publish_posts' ) ) {
return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) );
} }

View file

@ -1,409 +0,0 @@
<?php
/**
* This is the class for integrating ActivityPub into the Friends Plugin.
*
* @since 0.14
*
* @package ActivityPub
* @author Alex Kirk
*/
namespace Activitypub;
class Friends_Feed_Parser_ActivityPub extends \Friends\Feed_Parser {
const SLUG = 'activitypub';
const NAME = 'ActivityPub';
const URL = 'https://www.w3.org/TR/activitypub/';
private $friends_feed;
/**
* Constructor.
*
* @param \Friends\Feed $friends_feed The friends feed
*/
public function __construct( \Friends\Feed $friends_feed ) {
$this->friends_feed = $friends_feed;
\add_action( 'activitypub_inbox', array( $this, 'handle_received_activity' ), 10, 3 );
\add_action( 'friends_user_feed_activated', array( $this, 'queue_follow_user' ), 10 );
\add_action( 'friends_user_feed_deactivated', array( $this, 'queue_unfollow_user' ), 10 );
\add_action( 'friends_feed_parser_activitypub_follow', array( $this, 'follow_user' ), 10, 2 );
\add_action( 'friends_feed_parser_activitypub_unfollow', array( $this, 'unfollow_user' ), 10, 2 );
\add_filter( 'friends_rewrite_incoming_url', array( $this, 'friends_rewrite_incoming_url' ), 10, 2 );
}
/**
* Allow logging a message via an action.
* @param string $message The message to log.
* @param array $objects Optional objects as meta data.
* @return void
*/
private function log( $message, $objects = array() ) {
do_action( 'friends_activitypub_log', $message, $objects );
}
/**
* Determines if this is a supported feed and to what degree we feel it's supported.
*
* @param string $url The url.
* @param string $mime_type The mime type.
* @param string $title The title.
* @param string|null $content The content, it can't be assumed that it's always available.
*
* @return int Return 0 if unsupported, a positive value representing the confidence for the feed, use 10 if you're reasonably confident.
*/
public function feed_support_confidence( $url, $mime_type, $title, $content = null ) {
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $url ) ) {
return 10;
}
return 0;
}
/**
* Format the feed title and autoselect the posts feed.
*
* @param array $feed_details The feed details.
*
* @return array The (potentially) modified feed details.
*/
public function update_feed_details( $feed_details ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $feed_details['url'] );
if ( ! $meta || is_wp_error( $meta ) ) {
return $meta;
}
if ( isset( $meta['name'] ) ) {
$feed_details['title'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$feed_details['title'] = $meta['preferredUsername'];
}
if ( isset( $meta['id'] ) ) {
$feed_details['url'] = $meta['id'];
}
return $feed_details;
}
/**
* Rewrite a Mastodon style URL @username@server to a URL via webfinger.
*
* @param string $url The URL to filter.
* @param string $incoming_url Potentially a mastodon identifier.
*
* @return <type> ( description_of_the_return_value )
*/
public function friends_rewrite_incoming_url( $url, $incoming_url ) {
if ( preg_match( '/^@?[^@]+@((?:[a-z0-9-]+\.)+[a-z]+)$/i', $incoming_url ) ) {
$resolved_url = \Activitypub\Webfinger::resolve( $incoming_url );
if ( ! is_wp_error( $resolved_url ) ) {
return $resolved_url;
}
}
return $url;
}
/**
* Discover the feeds available at the URL specified.
*
* @param string $content The content for the URL is already provided here.
* @param string $url The url to search.
*
* @return array A list of supported feeds at the URL.
*/
public function discover_available_feeds( $content, $url ) {
$discovered_feeds = array();
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
if ( $meta && ! is_wp_error( $meta ) ) {
$discovered_feeds[ $meta['id'] ] = array(
'type' => 'application/activity+json',
'rel' => 'self',
'post-format' => 'status',
'parser' => self::SLUG,
'autoselect' => true,
);
}
return $discovered_feeds;
}
/**
* Fetches a feed and returns the processed items.
*
* @param string $url The url.
*
* @return array An array of feed items.
*/
public function fetch_feed( $url ) {
// There is no feed to fetch, we'll receive items via ActivityPub.
return array();
}
/**
* Handles "Create" requests
*
* @param array $object The activity-object
* @param int $user_id The id of the local blog-user
* @param string $type The type of the activity.
*/
public function handle_received_activity( $object, $user_id, $type ) {
if ( ! in_array(
$type,
array(
// We don't need to handle 'Accept' types since it's handled by the ActivityPub plugin itself.
'create',
'announce',
),
true
) ) {
return false;
}
$actor_url = $object['actor'];
$user_feed = false;
if ( \wp_http_validate_url( $actor_url ) ) {
// Let's check if we follow this actor. If not it might be a different URL representation.
$user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
}
if ( is_wp_error( $user_feed ) || ! \wp_http_validate_url( $actor_url ) ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $actor_url );
if ( ! $meta || ! isset( $meta['url'] ) ) {
$this->log( 'Received invalid meta for ' . $actor_url );
return false;
}
$actor_url = $meta['url'];
if ( ! \wp_http_validate_url( $actor_url ) ) {
$this->log( 'Received invalid meta url for ' . $actor_url );
return false;
}
}
$user_feed = $this->friends_feed->get_user_feed_by_url( $actor_url );
if ( ! $user_feed || is_wp_error( $user_feed ) ) {
$this->log( 'We\'re not following ' . $actor_url );
// We're not following this user.
return false;
}
switch ( $type ) {
case 'create':
return $this->handle_incoming_post( $object['object'], $user_feed );
case 'announce':
return $this->handle_incoming_announce( $object['object'], $user_feed, $user_id );
}
return true;
}
/**
* Map the Activity type to a post fomat.
*
* @param string $type The type.
*
* @return string The determined post format.
*/
private function map_type_to_post_format( $type ) {
return 'status';
}
/**
* We received a post for a feed, handle it.
*
* @param array $object The object from ActivityPub.
* @param \Friends\User_Feed $user_feed The user feed.
*/
private function handle_incoming_post( $object, \Friends\User_Feed $user_feed ) {
$permalink = $object['id'];
if ( isset( $object['url'] ) ) {
$permalink = $object['url'];
}
$data = array(
'permalink' => $permalink,
'content' => $object['content'],
'post_format' => $this->map_type_to_post_format( $object['type'] ),
'date' => $object['published'],
);
if ( isset( $object['attributedTo'] ) ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] );
$this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) );
if ( isset( $meta['name'] ) ) {
$data['author'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$data['author'] = $meta['preferredUsername'];
}
}
if ( ! empty( $object['attachment'] ) ) {
foreach ( $object['attachment'] as $attachment ) {
if ( ! isset( $attachment['type'] ) || ! isset( $attachment['mediaType'] ) ) {
continue;
}
if ( 'Document' !== $attachment['type'] || strpos( $attachment['mediaType'], 'image/' ) !== 0 ) {
continue;
}
$data['content'] .= PHP_EOL;
$data['content'] .= '<!-- wp:image -->';
$data['content'] .= '<p><img src="' . esc_url( $attachment['url'] ) . '" width="' . esc_attr( $attachment['width'] ) . '" height="' . esc_attr( $attachment['height'] ) . '" class="size-full" /></p>';
$data['content'] .= '<!-- /wp:image -->';
}
$meta = \Activitypub\get_remote_metadata_by_actor( $object['attributedTo'] );
$this->log( 'Attributed to ' . $object['attributedTo'], compact( 'meta' ) );
if ( isset( $meta['name'] ) ) {
$data['author'] = $meta['name'];
} elseif ( isset( $meta['preferredUsername'] ) ) {
$data['author'] = $meta['preferredUsername'];
}
}
$this->log(
'Received feed item',
array(
'url' => $permalink,
'data' => $data,
)
);
$item = new \Friends\Feed_Item( $data );
$this->friends_feed->process_incoming_feed_items( array( $item ), $user_feed );
return true;
}
/**
* We received an announced URL (boost) for a feed, handle it.
*
* @param array $url The announced URL.
* @param \Friends\User_Feed $user_feed The user feed.
*/
private function handle_incoming_announce( $url, \Friends\User_Feed $user_feed, $user_id ) {
if ( ! \wp_http_validate_url( $url ) ) {
$this->log( 'Received invalid announce', compact( 'url' ) );
return false;
}
$this->log( 'Received announce for ' . $url );
$response = \Activitypub\safe_remote_get( $url, $user_id );
if ( \is_wp_error( $response ) ) {
return $response;
}
$json = \wp_remote_retrieve_body( $response );
$object = \json_decode( $json, true );
if ( ! $object ) {
$this->log( 'Received invalid json', compact( 'json' ) );
return false;
}
$this->log( 'Received response', compact( 'url', 'object' ) );
return $this->handle_incoming_post( $object, $user_feed );
}
/**
* Prepare to follow the user via a scheduled event.
*
* @param \Friends\User_Feed $user_feed The user feed.
*
* @return bool|WP_Error Whether the event was queued.
*/
public function queue_follow_user( \Friends\User_Feed $user_feed ) {
if ( self::SLUG !== $user_feed->get_parser() ) {
return;
}
$args = array( $user_feed->get_url(), get_current_user_id() );
$unfollow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args );
if ( $unfollow_timestamp ) {
// If we just unfollowed, we don't want the event to potentially be executed after our follow event.
wp_unschedule_event( $unfollow_timestamp, $args );
}
if ( wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args ) ) {
return;
}
return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_follow', $args );
}
/**
* Follow a user via ActivityPub at a URL.
*
* @param string $url The url.
* @param int $user_id The current user id.
*/
public function follow_user( $url, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
$to = $meta['id'];
$inbox = \Activitypub\get_inbox_by_actor( $to );
$actor = \get_author_posts_url( $user_id );
$activity = new \Activitypub\Model\Activity( 'Follow', \Activitypub\Model\Activity::TYPE_SIMPLE );
$activity->set_to( null );
$activity->set_cc( null );
$activity->set_actor( $actor );
$activity->set_object( $to );
$activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity = $activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
}
/**
* Prepare to unfollow the user via a scheduled event.
*
* @param \Friends\User_Feed $user_feed The user feed.
*
* @return bool|WP_Error Whether the event was queued.
*/
public function queue_unfollow_user( \Friends\User_Feed $user_feed ) {
if ( self::SLUG !== $user_feed->get_parser() ) {
return false;
}
$args = array( $user_feed->get_url(), get_current_user_id() );
$follow_timestamp = wp_next_scheduled( 'friends_feed_parser_activitypub_follow', $args );
if ( $follow_timestamp ) {
// If we just followed, we don't want the event to potentially be executed after our unfollow event.
wp_unschedule_event( $follow_timestamp, $args );
}
if ( wp_next_scheduled( 'friends_feed_parser_activitypub_unfollow', $args ) ) {
return true;
}
return \wp_schedule_single_event( \time(), 'friends_feed_parser_activitypub_unfollow', $args );
}
/**
* Unfllow a user via ActivityPub at a URL.
*
* @param string $url The url.
* @param int $user_id The current user id.
*/
public function unfollow_user( $url, $user_id ) {
$meta = \Activitypub\get_remote_metadata_by_actor( $url );
$to = $meta['id'];
$inbox = \Activitypub\get_inbox_by_actor( $to );
$actor = \get_author_posts_url( $user_id );
$activity = new \Activitypub\Model\Activity( 'Undo', \Activitypub\Model\Activity::TYPE_SIMPLE );
$activity->set_to( null );
$activity->set_cc( null );
$activity->set_actor( $actor );
$activity->set_object(
array(
'type' => 'Follow',
'actor' => $actor,
'object' => $to,
'id' => $to,
)
);
$activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) );
$activity = $activity->to_json();
\Activitypub\safe_remote_post( $inbox, $activity, $user_id );
}
}

View file

@ -7,7 +7,7 @@
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
> >
<testsuites> <testsuites>
<testsuite> <testsuite name="ActivityPub">
<directory prefix="test-" suffix=".php">./tests/</directory> <directory prefix="test-" suffix=".php">./tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>

View file

@ -88,6 +88,17 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub). Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github.com/pfefferle/wordpress-activitypub).
= v.next =
* Add configuration item for number of images to attach. props [@mexon](https://github.com/mexon)
* Use shortcodes instead of custom templates, to setup the Activity Post-Content. props [@toolstack](https://github.com/toolstack)
= 0.15.0 =
* Enable ActivityPub only for users that can `publish_posts`
* Persist only public Activities
* Fix remote-delete
= 0.14.3 = = 0.14.3 =
* Better error handling. props [@akirk](https://github.com/akirk) * Better error handling. props [@akirk](https://github.com/akirk)
@ -102,7 +113,7 @@ Project maintained on GitHub at [pfefferle/wordpress-activitypub](https://github
= 0.14.0 = = 0.14.0 =
* Friends support: https://wordpress.org/plugins/friends/ . props [@akirk](https://github.com/akirk) * Friends support: https://wordpress.org/plugins/friends/ props [@akirk](https://github.com/akirk)
* Massive guidance improvements. props [mediaformat](https://github.com/mediaformat) & [@akirk](https://github.com/akirk) * Massive guidance improvements. props [mediaformat](https://github.com/mediaformat) & [@akirk](https://github.com/akirk)
* Add Custom Post Type support to outbox API. props [blueset](https://github.com/blueset) * Add Custom Post Type support to outbox API. props [blueset](https://github.com/blueset)
* Better hash-tag support. props [bocops](https://github.com/bocops) * Better hash-tag support. props [bocops](https://github.com/bocops)

View file

@ -38,7 +38,7 @@ $json->manuallyApprovesFollowers = \apply_filters( 'activitypub_json_manually_ap
$json->publicKey = array( $json->publicKey = array(
'id' => \get_home_url( '/' ) . '#main-key', 'id' => \get_home_url( '/' ) . '#main-key',
'owner' => \get_home_url( '/' ), 'owner' => \get_home_url( '/' ),
'publicKeyPem' => \trim(), 'publicKeyPem' => '',
); );
$json->tag = array(); $json->tag = array();

View file

@ -56,22 +56,42 @@
<p> <p>
<textarea name="activitypub_custom_post_content" id="activitypub_custom_post_content" rows="10" cols="50" class="large-text" placeholder="<?php echo wp_kses( ACTIVITYPUB_CUSTOM_POST_CONTENT, 'post' ); ?>"><?php echo wp_kses( \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ), 'post' ); ?></textarea> <textarea name="activitypub_custom_post_content" id="activitypub_custom_post_content" rows="10" cols="50" class="large-text" placeholder="<?php echo wp_kses( ACTIVITYPUB_CUSTOM_POST_CONTENT, 'post' ); ?>"><?php echo wp_kses( \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ), 'post' ); ?></textarea>
<details> <details>
<summary><?php esc_html_e( 'See the complete list of template patterns.', 'activitypub' ); ?></summary> <summary><?php esc_html_e( 'See a list of ActivityPub Template Tags.', 'activitypub' ); ?></summary>
<div class="description"> <div class="description">
<ul> <ul>
<li><code>%title%</code> - <?php \esc_html_e( 'The Post-Title.', 'activitypub' ); ?></li> <li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li>
<li><code>%content%</code> - <?php \esc_html_e( 'The Post-Content.', 'activitypub' ); ?></li> <li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li>
<li><code>%excerpt%</code> - <?php \esc_html_e( 'The Post-Excerpt (default 400 Chars).', 'activitypub' ); ?></li> <li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (default 400 chars).', 'activitypub' ); ?></li>
<li><code>%permalink%</code> - <?php \esc_html_e( 'The Post-Permalink.', 'activitypub' ); ?></li> <li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
<?php // translators: ?> <li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
<li><code>%shortlink%</code> - <?php echo \wp_kses( \__( 'The Post-Shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>, to prettify the Shortlinks', 'activitypub' ), 'default' ); ?></li> <li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
<li><code>%hashtags%</code> - <?php \esc_html_e( 'The Tags as Hashtags.', 'activitypub' ); ?></li> <li><code>[ap_hashcats]</code> - <?php \esc_html_e( 'The post\'s categories as hashtags.', 'activitypub' ); ?></li>
<li><code>[ap_image]</code> - <?php \esc_html_e( 'The URL for the post\'s featured image.', 'activitypub' ); ?></li>
</ul> </ul>
<p><?php \esc_html_e( 'You can find the full list with all possible attributes in the help section on the top-right of the screen.', 'activitypub' ); ?></p>
</div> </div>
</details> </details>
</p> </p>
<?php // translators: ?> </td>
<p><?php echo \wp_kses( \__( '<a href="https://github.com/pfefferle/wordpress-activitypub/issues/new" target="_blank">Let me know</a> if you miss a template pattern.', 'activitypub' ), 'default' ); ?></p> </tr>
<tr>
<th scope="row">
<?php \esc_html_e( 'Number of images', 'activitypub' ); ?>
</th>
<td>
<input value="<?php echo esc_attr( \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ); ?>" name="activitypub_max_image_attachments" id="activitypub_max_image_attachments" type="number" min="0" />
<p class="description">
<?php
echo \wp_kses(
\sprintf(
// translators:
\__( 'The number of images to attach to posts. Default: <code>%s</code>', 'activitypub' ),
\esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
),
'default'
);
?>
</p>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -112,31 +132,11 @@
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
<?php \esc_html_e( 'Hashtags', 'activitypub' ); ?> <?php \esc_html_e( 'Hashtags (beta)', 'activitypub' ); ?>
</th> </th>
<td> <td>
<p> <p>
<label><input type="checkbox" name="activitypub_use_hashtags" id="activitypub_use_hashtags" value="1" <?php echo \checked( '1', \get_option( 'activitypub_use_hashtags', '1' ) ); ?> /> <?php echo wp_kses( \__( 'Add hashtags in the content as native tags and replace the <code>#tag</code> with the tag-link.', 'activitypub' ), 'default' ); ?></label> <label><input type="checkbox" name="activitypub_use_hashtags" id="activitypub_use_hashtags" value="1" <?php echo \checked( '1', \get_option( 'activitypub_use_hashtags', '1' ) ); ?> /> <?php echo wp_kses( \__( 'Add hashtags in the content as native tags and replace the <code>#tag</code> with the tag-link. <strong>This feature is experimental! Please disable it, if you find any HTML or CSS errors.</strong>', 'activitypub' ), 'default' ); ?></label>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php \esc_html_e( 'HTML Allowlist', 'activitypub' ); ?>
</th>
<td>
<textarea name="activitypub_allowed_html" id="activitypub_allowed_html" rows="3" cols="50" class="large-text"><?php echo esc_html( \get_option( 'activitypub_allowed_html', ACTIVITYPUB_ALLOWED_HTML ) ); ?></textarea>
<p class="description">
<?php
echo \wp_kses(
\sprintf(
// translators:
\__( 'A list of HTML elements, you want to allowlist for your activities. <strong>Leave list empty to support all HTML elements</strong>. Default: <code>%s</code>', 'activitypub' ),
\esc_html( ACTIVITYPUB_ALLOWED_HTML )
),
'default'
);
?>
</p> </p>
</td> </td>
</tr> </tr>

View file

@ -19,14 +19,9 @@ require_once $_tests_dir . '/includes/functions.php';
*/ */
function _manually_load_plugin() { function _manually_load_plugin() {
require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php'; require \dirname( \dirname( __FILE__ ) ) . '/activitypub.php';
// Load the Friends plugin if available to test the integrations.
$friends_plugin = \dirname( \dirname( \dirname( __FILE__ ) ) ) . '/friends/friends.php';
if ( file_exists( $friends_plugin ) ) {
require $friends_plugin;
}
} }
\tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' ); \tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
// Start up the WP testing environment. // Start up the WP testing environment.
require $_tests_dir . '/includes/bootstrap.php'; require $_tests_dir . '/includes/bootstrap.php';
require __DIR__ . '/class-activitypub-testcase-cache-http.php';

View file

@ -0,0 +1,105 @@
<?php
class ActivityPub_TestCase_Cache_HTTP extends \WP_UnitTestCase {
public $server;
public function set_up() {
parent::set_up();
// Manually activate the REST server.
global $wp_rest_server;
$wp_rest_server = new \Spy_REST_Server();
$this->server = $wp_rest_server;
do_action( 'rest_api_init' );
add_filter(
'rest_url',
function() {
return get_option( 'home' ) . '/wp-json/';
}
);
add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 );
add_filter( 'http_response', array( get_called_class(), 'http_response' ), 10, 3 );
}
public function tear_down() {
remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) );
remove_filter( 'http_response', array( get_called_class(), 'http_response' ) );
parent::tear_down();
}
public static function pre_http_request( $preempt, $request, $url ) {
$p = wp_parse_url( $url );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.json';
if ( file_exists( $cache ) ) {
return apply_filters(
'fake_http_response',
json_decode( file_get_contents( $cache ), true ), // phpcs:ignore
$p['scheme'] . '://' . $p['host'],
$url,
$request
);
}
$home_url = home_url();
// Pretend the url now is the requested one.
update_option( 'home', $p['scheme'] . '://' . $p['host'] );
$rest_prefix = home_url() . '/wp-json';
if ( false === strpos( $url, $rest_prefix ) ) {
// Restore the old home_url.
update_option( 'home', $home_url );
return $preempt;
}
$url = substr( $url, strlen( $rest_prefix ) );
$r = new \WP_REST_Request( $request['method'], $url );
if ( ! empty( $request['body'] ) ) {
foreach ( $request['body'] as $key => $value ) {
$r->set_param( $key, $value );
}
}
global $wp_rest_server;
$response = $wp_rest_server->dispatch( $r );
// Restore the old url.
update_option( 'home', $home_url );
return apply_filters(
'fake_http_response',
array(
'headers' => array(
'content-type' => 'text/json',
),
'body' => wp_json_encode( $response->data ),
'response' => array(
'code' => $response->status,
),
),
$p['scheme'] . '://' . $p['host'],
$url,
$request
);
}
public static function http_response( $response, $args, $url ) {
$p = wp_parse_url( $url );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.json';
if ( ! file_exists( $cache ) ) {
$headers = wp_remote_retrieve_headers( $response );
file_put_contents( // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_file_put_contents
$cache,
wp_json_encode(
array(
'headers' => $headers->getAll(),
'body' => wp_remote_retrieve_body( $response ),
'response' => array(
'code' => wp_remote_retrieve_response_code( $response ),
),
)
)
);
}
return $response;
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,22 @@
{
"headers": {
"date": "Fri, 09 Dec 2022 10:39:51 GMT",
"content-type": "application\/activity+json",
"server": "nginx",
"x-xrds-location": "https:\/\/notiz.blog\/?xrds",
"x-yadis-location": "https:\/\/notiz.blog\/?xrds",
"link": "<https:\/\/notiz.blog\/wp-api\/micropub\/1.0\/media>; rel=\"micropub_media\", <https:\/\/notiz.blog\/wp-api\/micropub\/1.0\/endpoint>; rel=\"micropub\", <https:\/\/notiz.blog\/wp-api\/friends\/v1>; rel=\"friends-base-url\", <https:\/\/notiz.blog\/wp-api\/indieauth\/1.0\/auth>; rel=\"authorization_endpoint\", <https:\/\/notiz.blog\/wp-api\/indieauth\/1.0\/token>; rel=\"token_endpoint\", <https:\/\/notiz.blog\/wp-api\/indieauth\/1.0\/metadata>; rel=\"indieauth-metadata\", <https:\/\/notiz.blog\/wp-api\/>; rel=\"https:\/\/api.w.org\/\", <https:\/\/notiz.blog\/wp-api\/wp\/v2\/users\/1>; rel=\"alternate\"; type=\"application\/json\"",
"cache-control": "max-age=0, public",
"expires": "Fri, 09 Dec 2022 10:39:51 GMT",
"x-xss-protection": "1; mode=block",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000",
"x-frame-options": "SAMEORIGIN",
"referrer-policy": "strict-origin-when-cross-origin",
"x-clacks-overhead": "GNU Terry Pratchett"
},
"body": "{\"@context\":[\"https:\\\/\\\/www.w3.org\\\/ns\\\/activitystreams\",\"https:\\\/\\\/w3id.org\\\/security\\\/v1\",{\"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\":{\"@id\":\"toot:featured\",\"@type\":\"@id\"},\"featuredTags\":{\"@id\":\"toot:featuredTags\",\"@type\":\"@id\"}}],\"id\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\",\"type\":\"Person\",\"name\":\"Matthias Pfefferle\",\"summary\":\"Ich bin Webworker und arbeite als \\u0022Head of WordPress Development\\u0022 f\\u00fcr IONOS in Karlsruhe. Ich blogge, podcaste und schreibe \\u003Cdel\\u003Eeine Kolumne\\u003C\\\/del\\u003E \\u00fcber das open, independent und federated social Web. \\u003Ca href=\\u0022https:\\\/\\\/notiz.blog\\\/about\\\/\\u0022\\u003EMehr \\u00fcber mich.\\u003C\\\/a\\u003E\",\"preferredUsername\":\"pfefferle\",\"url\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\",\"icon\":{\"type\":\"Image\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/75512bb584bbceae57dfc503692b16b2?s=120\\u0026d=mm\\u0026r=g\"},\"image\":{\"type\":\"Image\",\"url\":\"https:\\\/\\\/notiz.blog\\\/wp-content\\\/uploads\\\/2017\\\/02\\\/cropped-Unknown-2.jpeg\"},\"inbox\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/activitypub\\\/1.0\\\/users\\\/1\\\/inbox\",\"outbox\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/activitypub\\\/1.0\\\/users\\\/1\\\/outbox\",\"followers\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/activitypub\\\/1.0\\\/users\\\/1\\\/followers\",\"following\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/activitypub\\\/1.0\\\/users\\\/1\\\/following\",\"manuallyApprovesFollowers\":false,\"publicKey\":{\"id\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/#main-key\",\"owner\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\",\"publicKeyPem\":\"-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA039CnlArzn6nsRjcC2RJ\\nrjY3K5ZrLnFUbPtHLGNXMJUGW+rFYE1DzhdKPTj9giiXE+J7ADI0Tme5rSWw14bT\\nLhOMBs2ma8d03\\\/wnF1+kxDBeRyvyoki2TjtiJdoPu1jwZLLYTuzWTXdDiqrwSKOL\\nncKFGIkjyzOLoYuIKPgIuFg3Mt8rI6teQ2Q65YsGvOG\\\/mjBOUwl5FjgcGt9aQARd\\nmFxW5XydxfNrCZwuE34Zbq\\\/IC7rvaUx98zvrEHrD237YQ8O4M3afC9Kbu5Xp7k8Q\\n5JG80RItV7n8xjyt0i9LaVwlZDDYmLDYv50VhjcwRvtVFVfaN7yxDnHttd1NNENK\\nCwIDAQAB\\n-----END PUBLIC KEY-----\"},\"tag\":[],\"attachment\":[{\"type\":\"PropertyValue\",\"name\":\"Blog\",\"value\":\"\\u003Ca rel=\\u0022me\\u0022 title=\\u0022https:\\\/\\\/notiz.blog\\\/\\u0022 target=\\u0022_blank\\u0022 href=\\u0022https:\\\/\\\/notiz.blog\\\/\\u0022\\u003Enotiz.blog\\u003C\\\/a\\u003E\"},{\"type\":\"PropertyValue\",\"name\":\"Profil\",\"value\":\"\\u003Ca rel=\\u0022me\\u0022 title=\\u0022https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\\u0022 target=\\u0022_blank\\u0022 href=\\u0022https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\\u0022\\u003Enotiz.blog\\u003C\\\/a\\u003E\"},{\"type\":\"PropertyValue\",\"name\":\"Website\",\"value\":\"\\u003Ca rel=\\u0022me\\u0022 title=\\u0022https:\\\/\\\/pfefferle.org\\\/\\u0022 target=\\u0022_blank\\u0022 href=\\u0022https:\\\/\\\/pfefferle.org\\\/\\u0022\\u003Epfefferle.org\\u003C\\\/a\\u003E\"}]}",
"response": {
"code": 200
}
}

View file

@ -0,0 +1,22 @@
{
"headers": {
"date": "Fri, 09 Dec 2022 10:39:51 GMT",
"content-type": "application\/jrd+json; charset=UTF-8",
"server": "nginx",
"x-xrds-location": "https:\/\/notiz.blog\/?xrds",
"x-yadis-location": "https:\/\/notiz.blog\/?xrds",
"access-control-allow-origin": "*",
"cache-control": "max-age=2592000, public",
"expires": "Sun, 08 Jan 2023 10:39:50 GMT",
"x-xss-protection": "1; mode=block",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000",
"x-frame-options": "SAMEORIGIN",
"referrer-policy": "strict-origin-when-cross-origin",
"x-clacks-overhead": "GNU Terry Pratchett"
},
"body": "{\"subject\":\"acct:pfefferle@notiz.blog\",\"aliases\":[\"acct:pfefferle@notiz.blog\",\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\",\"mailto:pfefferle@notiz.blog\"],\"links\":[{\"rel\":\"http:\\\/\\\/webfinger.net\\\/rel\\\/profile-page\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\",\"type\":\"text\\\/html\"},{\"rel\":\"http:\\\/\\\/webfinger.net\\\/rel\\\/avatar\",\"href\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/75512bb584bbceae57dfc503692b16b2?s=96&d=mm&r=g\"},{\"rel\":\"http:\\\/\\\/webfinger.net\\\/rel\\\/profile-page\",\"href\":\"https:\\\/\\\/pfefferle.org\\\/\",\"type\":\"text\\\/html\"},{\"rel\":\"payment\",\"href\":\"https:\\\/\\\/www.paypal.me\\\/matthiaspfefferle\"},{\"rel\":\"payment\",\"href\":\"https:\\\/\\\/liberapay.com\\\/pfefferle\\\/\"},{\"rel\":\"payment\",\"href\":\"https:\\\/\\\/notiz.blog\\\/donate\\\/\"},{\"rel\":\"payment\",\"href\":\"https:\\\/\\\/flattr.com\\\/@pfefferle\"},{\"href\":\"https:\\\/\\\/notiz.blog\\\/\",\"rel\":\"http:\\\/\\\/specs.openid.net\\\/auth\\\/2.0\\\/provider\"},{\"rel\":\"self\",\"type\":\"application\\\/activity+json\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/\"},{\"rel\":\"micropub_media\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/micropub\\\/1.0\\\/media\"},{\"rel\":\"micropub\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/micropub\\\/1.0\\\/endpoint\"},{\"rel\":\"http:\\\/\\\/nodeinfo.diaspora.software\\\/ns\\\/schema\\\/2.0\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/nodeinfo\\\/2.0\"},{\"rel\":\"http:\\\/\\\/nodeinfo.diaspora.software\\\/ns\\\/schema\\\/1.1\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/nodeinfo\\\/1.1\"},{\"rel\":\"http:\\\/\\\/nodeinfo.diaspora.software\\\/ns\\\/schema\\\/1.0\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/nodeinfo\\\/1.0\"},{\"rel\":\"https:\\\/\\\/feneas.org\\\/ns\\\/serviceinfo\",\"type\":\"application\\\/ld+json\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/serviceinfo\\\/1.0\",\"properties\":{\"https:\\\/\\\/feneas.org\\\/ns\\\/serviceinfo#software.name\":\"notizBlog\"}},{\"rel\":\"http:\\\/\\\/schemas.google.com\\\/g\\\/2010#updates-from\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/feed\\\/ostatus\\\/\",\"type\":\"application\\\/atom+xml\"},{\"rel\":\"http:\\\/\\\/ostatus.org\\\/schema\\\/1.0\\\/subscribe\",\"template\":\"https:\\\/\\\/notiz.blog\\\/?profile={uri}\"},{\"rel\":\"magic-public-key\",\"href\":\"data:application\\\/magic-public-key,RSA.039CnlArzn6nsRjcC2RJrjY3K5ZrLnFUbPtHLGNXMJUGW-rFYE1DzhdKPTj9giiXE-J7ADI0Tme5rSWw14bTLhOMBs2ma8d03_wnF1-kxDBeRyvyoki2TjtiJdoPu1jwZLLYTuzWTXdDiqrwSKOLncKFGIkjyzOLoYuIKPgIuFg3Mt8rI6teQ2Q65YsGvOG_mjBOUwl5FjgcGt9aQARdmFxW5XydxfNrCZwuE34Zbq_IC7rvaUx98zvrEHrD237YQ8O4M3afC9Kbu5Xp7k8Q5JG80RItV7n8xjyt0i9LaVwlZDDYmLDYv50VhjcwRvtVFVfaN7yxDnHttd1NNENKCw==.AQAB\"},{\"rel\":\"salmon\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/?salmon=endpoint\"},{\"rel\":\"http:\\\/\\\/salmon-protocol.org\\\/ns\\\/salmon-replies\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/?salmon=endpoint\"},{\"rel\":\"http:\\\/\\\/salmon-protocol.org\\\/ns\\\/salmon-mention\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/?salmon=endpoint\"},{\"rel\":\"feed\",\"type\":\"application\\\/stream+json\",\"title\":\"Activity-Streams 1.0 Feed\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/feed\\\/as1\\\/\"},{\"rel\":\"feed\",\"type\":\"application\\\/activity+json\",\"title\":\"Activity-Streams 2.0 Feed\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/feed\\\/as2\\\/\"},{\"rel\":\"http:\\\/\\\/oexchange.org\\\/spec\\\/0.8\\\/rel\\\/user-target\",\"href\":\"https:\\\/\\\/notiz.blog\\\/?oexchange=xrd\",\"type\":\"application\\\/xrd+xml\"},{\"rel\":\"http:\\\/\\\/a9.com\\\/-\\\/spec\\\/opensearch\\\/1.1\\\/\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/opensearch\\\/1.1\\\/document\",\"type\":\"application\\\/opensearchdescription+xml\"},{\"rel\":\"describedby\",\"href\":\"https:\\\/\\\/notiz.blog\\\/author\\\/matthias-pfefferle\\\/feed\\\/foaf\\\/\",\"type\":\"application\\\/rdf+xml\"},{\"rel\":\"webmention\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/webmention\\\/1.0\\\/endpoint\"},{\"rel\":\"http:\\\/\\\/webmention.org\\\/\",\"href\":\"https:\\\/\\\/notiz.blog\\\/wp-api\\\/webmention\\\/1.0\\\/endpoint\"}],\"properties\":{\"http:\\\/\\\/salmon-protocol.org\\\/ns\\\/magic-key\":\"RSA.039CnlArzn6nsRjcC2RJrjY3K5ZrLnFUbPtHLGNXMJUGW-rFYE1DzhdKPTj9giiXE-J7ADI0Tme5rSWw14bTLhOMBs2ma8d03_wnF1-kxDBeRyvyoki2TjtiJdoPu1jwZLLYTuzWTXdDiqrwSKOLncKFGIkjyzOLoYuIKPgIuFg3Mt8rI6teQ2Q65YsGvOG_mjBOUwl5FjgcGt9aQARdmFxW5XydxfNrCZwuE34Zbq_IC7rvaUx98zvrEHrD237YQ8O4M3afC9Kbu5Xp7k8Q5JG80RItV7n8xjyt0i9LaVwlZDDYmLDYv50VhjcwRvtVFVfaN7yxDnHttd1NNENKCw==.AQAB\"}}",
"response": {
"code": 200
}
}

View file

@ -0,0 +1,135 @@
<?php
class Test_Activitypub_Activity_Dispatcher extends ActivityPub_TestCase_Cache_HTTP {
public static $users = array(
'username@example.org' => array(
'url' => 'https://example.org/users/username',
'inbox' => 'https://example.org/users/username/inbox',
'name' => 'username',
),
'jon@example.com' => array(
'url' => 'https://example.com/author/jon',
'inbox' => 'https://example.com/author/jon/inbox',
'name' => 'jon',
),
);
public function test_dispatch_activity() {
$followers = array( 'https://example.com/author/jon', 'https://example.org/users/username' );
\update_user_meta( 1, 'activitypub_followers', $followers );
$post = \wp_insert_post(
array(
'post_author' => 1,
'post_content' => 'hello',
)
);
$pre_http_request = new MockAction();
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
$activitypub_post = new \Activitypub\Model\Post( $post );
\Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post );
$this->assertSame( 2, $pre_http_request->get_call_count() );
$all_args = $pre_http_request->get_args();
$first_call_args = array_shift( $all_args );
$this->assertEquals( 'https://example.com/author/jon/inbox', $first_call_args[2] );
$second_call_args = array_shift( $all_args );
$this->assertEquals( 'https://example.org/users/username/inbox', $second_call_args[2] );
remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 );
}
public function test_dispatch_mentions() {
$post = \wp_insert_post(
array(
'post_author' => 1,
'post_content' => '@alex hello',
)
);
self::$users['https://example.com/alex'] = array(
'url' => 'https://example.com/alex',
'inbox' => 'https://example.com/alex/inbox',
'name' => 'alex',
);
add_filter(
'activitypub_extract_mentions',
function( $mentions ) {
$mentions[] = 'https://example.com/alex';
return $mentions;
},
10
);
$pre_http_request = new MockAction();
add_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10, 3 );
$activitypub_post = new \Activitypub\Model\Post( $post );
\Activitypub\Activity_Dispatcher::send_post_activity( $activitypub_post );
$this->assertSame( 1, $pre_http_request->get_call_count() );
$all_args = $pre_http_request->get_args();
$first_call_args = $all_args[0];
$this->assertEquals( 'https://example.com/alex/inbox', $first_call_args[2] );
$body = json_decode( $first_call_args[1]['body'], true );
$this->assertArrayHasKey( 'id', $body );
remove_all_filters( 'activitypub_from_post_object' );
remove_filter( 'pre_http_request', array( $pre_http_request, 'filter' ), 10 );
}
public function set_up() {
parent::set_up();
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
_delete_all_posts();
}
public function tear_down() {
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
parent::tear_down();
}
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
if ( isset( self::$users[ $actor ] ) ) {
return self::$users[ $actor ];
}
foreach ( self::$users as $username => $data ) {
if ( $data['url'] === $actor ) {
return $data;
}
}
return $pre;
}
public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
return true;
}
return $in;
}
public static function http_request_args( $args, $url ) {
if ( in_array( wp_parse_url( $url, PHP_URL_HOST ), array( 'example.com', 'example.org' ), true ) ) {
$args['reject_unsafe_urls'] = false;
}
return $args;
}
public static function pre_http_request( $preempt, $request, $url ) {
return array(
'headers' => array(
'content-type' => 'text/json',
),
'body' => '',
'response' => array(
'code' => 202,
),
);
}
public static function http_response( $response, $args, $url ) {
return $response;
}
}

View file

@ -0,0 +1,31 @@
<?php
class Test_Activitypub_Activity extends WP_UnitTestCase {
public function test_activity_mentions() {
$post = \wp_insert_post(
array(
'post_author' => 1,
'post_content' => '@alex hello',
)
);
add_filter(
'activitypub_extract_mentions',
function( $mentions ) {
$mentions['@alex'] = 'https://example.com/alex';
return $mentions;
},
10
);
$activitypub_post = new \Activitypub\Model\Post( $post );
$activitypub_activity = new \Activitypub\Model\Activity( 'Create', \Activitypub\Model\Activity::TYPE_FULL );
$activitypub_activity->from_post( $activitypub_post );
$this->assertContains( \get_rest_url( null, '/activitypub/1.0/users/1/followers' ), $activitypub_activity->get_cc() );
$this->assertContains( 'https://example.com/alex', $activitypub_activity->get_cc() );
remove_all_filters( 'activitypub_extract_mentions' );
\wp_trash_post( $post );
}
}

View file

@ -4,26 +4,26 @@ class Test_Activitypub_Hashtag extends WP_UnitTestCase {
* @dataProvider the_content_provider * @dataProvider the_content_provider
*/ */
public function test_the_content( $content, $content_with_hashtag ) { public function test_the_content( $content, $content_with_hashtag ) {
$content = \Activitypub\Hashtag::the_content( $content );
$this->assertEquals( $content_with_hashtag, $content );
}
public function the_content_provider() {
\wp_create_term( 'object', 'post_tag' ); \wp_create_term( 'object', 'post_tag' );
$object = \get_term_by( 'name', 'object', 'post_tag' ); $object = \get_term_by( 'name', 'object', 'post_tag' );
$link = \get_term_link( $object, 'post_tag' ); $link = \get_term_link( $object, 'post_tag' );
$content = \Activitypub\Hashtag::the_content( $content );
$this->assertEquals( sprintf( $content_with_hashtag, $link ), $content );
}
public function the_content_provider() {
return array( return array(
array( 'test', 'test' ), array( 'test', 'test' ),
array( '#test', '#test' ), array( '#test', '#test' ),
array( 'hallo #test test', 'hallo #test test' ), array( 'hallo #test test', 'hallo #test test' ),
array( 'hallo #object test', 'hallo <a rel="tag" class="u-tag u-category" href="' . $link . '">#object</a> test' ), array( 'hallo #object test', 'hallo <a rel="tag" class="u-tag u-category" href="%s">#object</a> test' ),
array( '#object test', '<a rel="tag" class="u-tag u-category" href="' . $link . '">#object</a> test' ), array( '#object test', '<a rel="tag" class="u-tag u-category" href="%s">#object</a> test' ),
array( 'hallo <a href="http://test.test/#object">test</a> test', 'hallo <a href="http://test.test/#object">test</a> test' ), array( 'hallo <a href="http://test.test/#object">test</a> test', 'hallo <a href="http://test.test/#object">test</a> test' ),
array( 'hallo <a href="http://test.test/#object">#test</a> test', 'hallo <a href="http://test.test/#object">#test</a> test' ), array( 'hallo <a href="http://test.test/#object">#test</a> test', 'hallo <a href="http://test.test/#object">#test</a> test' ),
array( '<div>hallo #object test</div>', '<div>hallo <a rel="tag" class="u-tag u-category" href="' . $link . '">#object</a> test</div>' ), array( '<div>hallo #object test</div>', '<div>hallo <a rel="tag" class="u-tag u-category" href="%s">#object</a> test</div>' ),
array( '<div>hallo #object</div>', '<div>hallo <a rel="tag" class="u-tag u-category" href="' . $link . '">#object</a></div>' ), array( '<div>hallo #object</div>', '<div>hallo <a rel="tag" class="u-tag u-category" href="%s">#object</a></div>' ),
array( '<div>#object</div>', '<div>#object</div>' ), array( '<div>#object</div>', '<div>#object</div>' ),
array( '<a>#object</a>', '<a>#object</a>' ), array( '<a>#object</a>', '<a>#object</a>' ),
array( '<div style="color: #ccc;">object</a>', '<div style="color: #ccc;">object</a>' ), array( '<div style="color: #ccc;">object</a>', '<div style="color: #ccc;">object</a>' ),

View file

@ -0,0 +1,37 @@
<?php
class Test_Activitypub_Mention extends ActivityPub_TestCase_Cache_HTTP {
public static $users = array(
'username@example.org' => array(
'url' => 'https://example.org/users/username',
'name' => 'username',
),
);
/**
* @dataProvider the_content_provider
*/
public function test_the_content( $content, $content_with_mention ) {
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
$content = \Activitypub\Mention::the_content( $content );
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
$this->assertEquals( $content_with_mention, $content );
}
public function the_content_provider() {
return array(
array( 'hallo @username@example.org test', 'hallo <a rel="mention" class="u-url mention" href="https://example.org/users/username">@<span>username</span></a> test' ),
array( 'hallo @pfefferle@notiz.blog test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span></a> test' ),
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@<span>pfefferle</span>@notiz.blog</a> test' ),
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/author/matthias-pfefferle/">@pfefferle@notiz.blog</a> test' ),
array( 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test', 'hallo <a rel="mention" class="u-url mention" href="https://notiz.blog/@pfefferle/">@pfefferle@notiz.blog</a> test' ),
);
}
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
$actor = ltrim( $actor, '@' );
if ( isset( self::$users[ $actor ] ) ) {
return self::$users[ $actor ];
}
return $pre;
}
}

View file

@ -1,326 +0,0 @@
<?php
class Test_Friends_Feed_Parser_ActivityPub extends \WP_UnitTestCase {
public static $users = array();
private $friend_id;
private $friend_name;
private $actor;
public function test_incoming_post() {
$now = time() - 10;
$status_id = 123;
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$post_count = count( $posts );
// Let's post a new Note through the REST API.
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$content = 'Test ' . $date . ' ' . rand();
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Create' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', $this->actor );
$attachment_url = 'https://mastodon.local/files/original/1234.png';
$attachment_width = 400;
$attachment_height = 600;
$request->set_param(
'object',
array(
'type' => 'Note',
'id' => $id,
'attributedTo' => $this->actor,
'content' => $content,
'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ),
'published' => $date,
'attachment' => array(
array(
'type' => 'Document',
'mediaType' => 'image/png',
'url' => $attachment_url,
'name' => '',
'blurhash' => '',
'width' => $attachment_width,
'height' => $attachment_height,
),
),
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 1, count( $posts ) );
$this->assertStringStartsWith( $content, $posts[0]->post_content );
$this->assertStringContainsString( '<img src="' . esc_url( $attachment_url ) . '" width="' . esc_attr( $attachment_width ) . '" height="' . esc_attr( $attachment_height ) . '"', $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
// Do another test post, this time with a URL that has an @-id.
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$content = 'Test ' . $date . ' ' . rand();
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Create' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', 'https://mastodon.local/@akirk' );
$request->set_param(
'object',
array(
'type' => 'Note',
'id' => $id,
'attributedTo' => 'https://mastodon.local/@akirk',
'content' => $content,
'url' => 'https://mastodon.local/users/akirk/statuses/' . ( $status_id++ ),
'published' => $date,
)
);
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 2, count( $posts ) );
$this->assertEquals( $content, $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
$this->assertEquals( $this->friend_name, get_post_meta( $posts[0]->ID, 'author', true ) );
}
public function test_incoming_announce() {
$now = time() - 10;
$status_id = 123;
self::$users['https://notiz.blog/author/matthias-pfefferle/'] = array(
'url' => 'https://notiz.blog/author/matthias-pfefferle/',
'name' => 'Matthias Pfefferle',
);
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$post_count = count( $posts );
$date = gmdate( \DATE_W3C, $now++ );
$id = 'test' . $status_id;
$object = 'https://notiz.blog/2022/11/14/the-at-protocol/';
$request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/' . get_current_user_id() . '/inbox' );
$request->set_param( 'type', 'Announce' );
$request->set_param( 'id', $id );
$request->set_param( 'actor', $this->actor );
$request->set_param( 'published', $date );
$request->set_param( 'object', $object );
$response = $this->server->dispatch( $request );
$this->assertEquals( 202, $response->get_status() );
$p = wp_parse_url( $object );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response';
$this->assertFileExists( $cache );
$object = json_decode( wp_remote_retrieve_body( unserialize( file_get_contents( $cache ) ) ) );
$posts = get_posts(
array(
'post_type' => \Friends\Friends::CPT,
'author' => $this->friend_id,
)
);
$this->assertEquals( $post_count + 1, count( $posts ) );
$this->assertStringContainsString( 'Dezentrale Netzwerke', $posts[0]->post_content );
$this->assertEquals( $this->friend_id, $posts[0]->post_author );
$this->assertEquals( 'Matthias Pfefferle', get_post_meta( $posts[0]->ID, 'author', true ) );
}
public function set_up() {
if ( ! class_exists( '\Friends\Friends' ) ) {
return $this->markTestSkipped( 'The Friends plugin is not loaded.' );
}
parent::set_up();
// Manually activate the REST server.
global $wp_rest_server;
$wp_rest_server = new \Spy_REST_Server();
$this->server = $wp_rest_server;
do_action( 'rest_api_init' );
add_filter(
'rest_url',
function() {
return get_option( 'home' ) . '/wp-json/';
}
);
add_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ), 10, 3 );
add_filter( 'http_response', array( get_called_class(), 'http_response' ), 10, 3 );
add_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ), 10, 2 );
add_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ), 10, 2 );
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
$user_id = $this->factory->user->create(
array(
'role' => 'administrator',
)
);
wp_set_current_user( $user_id );
$this->friend_name = 'Alex Kirk';
$this->actor = 'https://mastodon.local/users/akirk';
$user_feed = \Friends\User_Feed::get_by_url( $this->actor );
if ( is_wp_error( $user_feed ) ) {
$this->friend_id = $this->factory->user->create(
array(
'display_name' => $this->friend_name,
'role' => 'friend',
)
);
\Friends\User_Feed::save(
new \Friends\User( $this->friend_id ),
$this->actor,
array(
'parser' => 'activitypub',
)
);
} else {
$this->friend_id = $user_feed->get_friend_user()->ID;
}
self::$users[ $this->actor ] = array(
'url' => $this->actor,
'name' => $this->friend_name,
);
self::$users['https://mastodon.local/@akirk'] = self::$users[ $this->actor ];
_delete_all_posts();
}
public function tear_down() {
remove_filter( 'pre_http_request', array( get_called_class(), 'pre_http_request' ) );
remove_filter( 'http_response', array( get_called_class(), 'http_response' ) );
remove_filter( 'http_request_host_is_external', array( get_called_class(), 'http_request_host_is_external' ) );
remove_filter( 'http_request_args', array( get_called_class(), 'http_request_args' ) );
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
}
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
if ( isset( self::$users[ $actor ] ) ) {
return self::$users[ $actor ];
}
return $pre;
}
public static function http_request_host_is_external( $in, $host ) {
if ( in_array( $host, array( 'mastodon.local' ), true ) ) {
return true;
}
return $in;
}
public static function http_request_args( $args, $url ) {
if ( in_array( parse_url( $url, PHP_URL_HOST ), array( 'mastodon.local' ), true ) ) {
$args['reject_unsafe_urls'] = false;
}
return $args;
}
public static function pre_http_request( $preempt, $request, $url ) {
$p = wp_parse_url( $url );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response';
if ( file_exists( $cache ) ) {
return apply_filters(
'fake_http_response',
unserialize( file_get_contents( $cache ) ),
$p['scheme'] . '://' . $p['host'],
$url,
$request
);
}
$home_url = home_url();
// Pretend the url now is the requested one.
update_option( 'home', $p['scheme'] . '://' . $p['host'] );
$rest_prefix = home_url() . '/wp-json';
if ( false === strpos( $url, $rest_prefix ) ) {
// Restore the old home_url.
update_option( 'home', $home_url );
return $preempt;
}
$url = substr( $url, strlen( $rest_prefix ) );
$r = new \WP_REST_Request( $request['method'], $url );
if ( ! empty( $request['body'] ) ) {
foreach ( $request['body'] as $key => $value ) {
$r->set_param( $key, $value );
}
}
global $wp_rest_server;
$response = $wp_rest_server->dispatch( $r );
// Restore the old url.
update_option( 'home', $home_url );
return apply_filters(
'fake_http_response',
array(
'headers' => array(
'content-type' => 'text/json',
),
'body' => wp_json_encode( $response->data ),
'response' => array(
'code' => $response->status,
),
),
$p['scheme'] . '://' . $p['host'],
$url,
$request
);
}
public static function http_response( $response, $args, $url ) {
$p = wp_parse_url( $url );
$cache = __DIR__ . '/fixtures/' . sanitize_title( $p['host'] . '-' . $p['path'] ) . '.response';
if ( ! file_exists( $cache ) ) {
$headers = wp_remote_retrieve_headers( $response );
file_put_contents(
$cache,
serialize(
array(
'headers' => $headers->getAll(),
'body' => wp_remote_retrieve_body( $response ),
'response' => array(
'code' => wp_remote_retrieve_response_code( $response ),
),
)
)
);
}
return $response;
}
}

9
tests/test-functions.php Normal file
View file

@ -0,0 +1,9 @@
<?php
class Test_Functions extends ActivityPub_TestCase_Cache_HTTP {
public function test_get_remote_metadata_by_actor() {
$metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' );
$this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
}
}