modified file upgrade-temp-backup
This commit is contained in:
@ -0,0 +1,3 @@
|
||||
# Integrations
|
||||
|
||||
This folder contains classes that take care of compatibility with other plugins.
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* Akismet integration.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\was_comment_received;
|
||||
|
||||
/**
|
||||
* Compatibility with the Akismet plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/akismet/
|
||||
*/
|
||||
class Akismet {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the "history" action from the comment row actions.
|
||||
*
|
||||
* @param array $actions The existing actions.
|
||||
* @param int|\WP_Comment $comment The comment object or ID.
|
||||
*
|
||||
* @return array The modified actions.
|
||||
*/
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['history'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* BuddyPress integration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Compatibility with the BuddyPress plugin.
|
||||
*
|
||||
* @see https://buddypress.org/
|
||||
*/
|
||||
class Buddypress {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_json_author_array', array( self::class, 'add_user_metadata' ), 11, 2 );
|
||||
\add_filter( 'render_block_activitypub/followers', array( self::class, 'escape_at_signs' ) );
|
||||
\add_filter( 'render_block_activitypub/following', array( self::class, 'escape_at_signs' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape `@` signs in block output to prevent BuddyPress mention linking.
|
||||
*
|
||||
* BuddyPress hooks `bp_activity_at_name_filter` into `the_content` to convert
|
||||
* `@username` mentions into profile links. This corrupts the JSON in the
|
||||
* `data-wp-context` attribute of Followers/Following blocks because the handles
|
||||
* contain `@username` patterns that match BuddyPress's regex.
|
||||
*
|
||||
* Encoding `@` as `@` in the HTML attribute makes it invisible to
|
||||
* BuddyPress's regex. The browser decodes the HTML entity before JavaScript
|
||||
* reads the attribute, so the Interactivity API receives the original `@`.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
*
|
||||
* @return string The block content with `@` signs escaped in data attributes.
|
||||
*/
|
||||
public static function escape_at_signs( $block_content ) {
|
||||
return \preg_replace_callback(
|
||||
'/data-wp-context="([^"]*)"/',
|
||||
static function ( $matches ) {
|
||||
return 'data-wp-context="' . \str_replace( '@', '@', $matches[1] ) . '"';
|
||||
},
|
||||
$block_content
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add BuddyPress user metadata to the author array.
|
||||
*
|
||||
* @param object $author The author object.
|
||||
* @param int $author_id The author ID.
|
||||
*
|
||||
* @return object The author object.
|
||||
*/
|
||||
public static function add_user_metadata( $author, $author_id ) {
|
||||
if ( \function_exists( 'bp_members_get_user_url' ) ) {
|
||||
$author->url = bp_members_get_user_url( $author_id );
|
||||
} else {
|
||||
$author->url = bp_core_get_user_domain( $author_id );
|
||||
}
|
||||
|
||||
// Add BuddyPress' cover_image instead of WordPress' header_image.
|
||||
$cover_image_url = bp_attachments_get_attachment( 'url', array( 'item_id' => $author_id ) );
|
||||
|
||||
if ( $cover_image_url ) {
|
||||
$author->image = array(
|
||||
'type' => 'Image',
|
||||
'url' => $cover_image_url,
|
||||
);
|
||||
}
|
||||
|
||||
// Change profile URL to BuddyPress' profile URL.
|
||||
$author->attachment['profile_url'] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => \__( 'Profile', 'activitypub' ),
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( $author->url ),
|
||||
\esc_url( $author->url ),
|
||||
\wp_parse_url( $author->url, \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
|
||||
// Replace blog URL on multisite.
|
||||
if ( is_multisite() ) {
|
||||
$user_blogs = get_blogs_of_user( $author_id ); // Get sites of user to send as AP metadata.
|
||||
|
||||
if ( ! empty( $user_blogs ) ) {
|
||||
unset( $author->attachment['blog_url'] );
|
||||
|
||||
foreach ( $user_blogs as $blog ) {
|
||||
if ( 1 !== $blog->userblog_id ) {
|
||||
$author->attachment[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => $blog->blogname,
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( $blog->siteurl ),
|
||||
$blog->siteurl,
|
||||
\wp_parse_url( $blog->siteurl, \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $author;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
/**
|
||||
* Classic Editor integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Classic Editor integration class.
|
||||
*
|
||||
* Handles compatibility with the Classic Editor plugin and sites without block editor support.
|
||||
*/
|
||||
class Classic_Editor {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_attachments_media_markup', array( self::class, 'filter_attachments_media_markup' ), 10, 2 );
|
||||
\add_filter( 'activitypub_attachment_ids', array( self::class, 'filter_attached_media_ids' ), 10, 2 );
|
||||
\add_action( 'add_meta_boxes', array( self::class, 'add_meta_box' ) );
|
||||
\add_action( 'save_post', array( self::class, 'save_meta_data' ) );
|
||||
|
||||
if ( \function_exists( 'classicpress_version' ) ) {
|
||||
\add_filter( 'activitypub_site_supports_blocks', '__return_false' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter attachment media markup to use shortcodes instead of blocks.
|
||||
*
|
||||
* @param string $markup The custom markup. Empty string by default.
|
||||
* @param array $attachment_ids Array of attachment IDs.
|
||||
*
|
||||
* @return string The generated shortcode markup.
|
||||
*/
|
||||
public static function filter_attachments_media_markup( $markup, $attachment_ids ) {
|
||||
if ( empty( $attachment_ids ) ) {
|
||||
return $markup;
|
||||
}
|
||||
|
||||
$type = strtok( \get_post_mime_type( $attachment_ids[0] ), '/' );
|
||||
|
||||
// Single video or audio file: use media shortcode.
|
||||
if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) {
|
||||
return sprintf(
|
||||
'[%1$s src="%2$s"]',
|
||||
\esc_attr( $type ),
|
||||
\esc_url( \wp_get_attachment_url( $attachment_ids[0] ) )
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple attachments or images: use gallery shortcode.
|
||||
return '[gallery ids="' . implode( ',', $attachment_ids ) . '" link="none"]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to add attached media IDs from the post's media library.
|
||||
*
|
||||
* Returns additional image attachments from the post's media library,
|
||||
* respecting the maximum image attachment limit. Only returns new
|
||||
* attachments that can be added without exceeding the limit.
|
||||
*
|
||||
* @param array $attachments The current list of attachments.
|
||||
* @param \WP_Post $item The post item.
|
||||
*
|
||||
* @return array Array of new media IDs to add (as associative arrays with 'id' key).
|
||||
* Returns empty array if already at or over the limit.
|
||||
*/
|
||||
public static function filter_attached_media_ids( $attachments, $item ) {
|
||||
$max_media = \get_option( 'activitypub_max_image_attachments', \ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS );
|
||||
$actual_count = \max( 0, $max_media - \count( $attachments ) );
|
||||
|
||||
if ( $actual_count <= 0 ) {
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
$query = new \WP_Query(
|
||||
array(
|
||||
'post_parent' => $item->ID,
|
||||
'post_status' => 'inherit',
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'image',
|
||||
'order' => 'ASC',
|
||||
'orderby' => 'menu_order ID',
|
||||
'fields' => 'ids',
|
||||
'posts_per_page' => $actual_count,
|
||||
)
|
||||
);
|
||||
|
||||
// Transform IDs into associative arrays.
|
||||
$media_ids = \array_map(
|
||||
static function ( $id ) {
|
||||
return array( 'id' => $id );
|
||||
},
|
||||
$query->get_posts()
|
||||
);
|
||||
|
||||
return \array_merge( $attachments, $media_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub meta box to the post editor.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
*/
|
||||
public static function add_meta_box( $post_type ) {
|
||||
// Only add for post types that support ActivityPub.
|
||||
if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
\add_meta_box(
|
||||
'activitypub-settings',
|
||||
\__( 'Fediverse ⁂', 'activitypub' ),
|
||||
array( self::class, 'render_meta_box' ),
|
||||
$post_type,
|
||||
'side',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the ActivityPub meta box.
|
||||
*
|
||||
* @param \WP_Post $post The post object.
|
||||
*/
|
||||
public static function render_meta_box( $post ) {
|
||||
// Add nonce for security.
|
||||
\wp_nonce_field( 'activitypub_meta_box', 'activitypub_meta_box_nonce' );
|
||||
|
||||
// Get current values.
|
||||
$content_warning = \get_post_meta( $post->ID, 'activitypub_content_warning', true );
|
||||
$max_image_attachments = \get_post_meta( $post->ID, 'activitypub_max_image_attachments', true );
|
||||
$content_visibility = self::get_default_visibility( $post );
|
||||
$default_quote_policy = \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE );
|
||||
$quote_interaction = \get_post_meta( $post->ID, 'activitypub_interaction_policy_quote', true ) ?: $default_quote_policy;
|
||||
|
||||
?>
|
||||
<p>
|
||||
<label for="activitypub_content_warning">
|
||||
<strong><?php \esc_html_e( 'Content Warning', 'activitypub' ); ?></strong>
|
||||
</label><br />
|
||||
<input type="text" id="activitypub_content_warning" name="activitypub_content_warning" value="<?php echo \esc_attr( $content_warning ); ?>" class="widefat" placeholder="<?php \esc_attr_e( 'Optional content warning', 'activitypub' ); ?>" />
|
||||
<span class="howto"><?php \esc_html_e( 'Content warnings do not change the content on your site, only in the fediverse.', 'activitypub' ); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="activitypub_max_image_attachments">
|
||||
<strong><?php \esc_html_e( 'Maximum Image Attachments', 'activitypub' ); ?></strong>
|
||||
</label><br />
|
||||
<input type="number" id="activitypub_max_image_attachments" name="activitypub_max_image_attachments" value="<?php echo \esc_attr( $max_image_attachments ); ?>" min="0" max="10" class="small-text" />
|
||||
<span class="howto"><?php \esc_html_e( 'Maximum number of image attachments to include when sharing to the fediverse.', 'activitypub' ); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong><?php \esc_html_e( 'Visibility', 'activitypub' ); ?></strong><br />
|
||||
<label>
|
||||
<input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); ?> />
|
||||
<?php \esc_html_e( 'Public', 'activitypub' ); ?>
|
||||
</label><br />
|
||||
<label>
|
||||
<input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); ?> />
|
||||
<?php \esc_html_e( 'Quiet public', 'activitypub' ); ?>
|
||||
</label><br />
|
||||
<label>
|
||||
<input type="radio" name="activitypub_content_visibility" value="<?php echo \esc_attr( ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ); ?>" <?php \checked( $content_visibility, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ); ?> />
|
||||
<?php \esc_html_e( 'Do not federate', 'activitypub' ); ?>
|
||||
</label><br />
|
||||
<span class="howto"><?php \esc_html_e( 'This adjusts the visibility of a post in the fediverse, but note that it won\'t affect how the post appears on the blog.', 'activitypub' ); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<label for="activitypub_interaction_policy_quote">
|
||||
<strong><?php \esc_html_e( 'Who can quote this post?', 'activitypub' ); ?></strong>
|
||||
</label><br />
|
||||
<select id="activitypub_interaction_policy_quote" name="activitypub_interaction_policy_quote" class="widefat">
|
||||
<option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_ANYONE ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_ANYONE ); ?>><?php \esc_html_e( 'Anyone', 'activitypub' ); ?></option>
|
||||
<option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS ); ?>><?php \esc_html_e( 'Followers only', 'activitypub' ); ?></option>
|
||||
<option value="<?php echo \esc_attr( ACTIVITYPUB_INTERACTION_POLICY_ME ); ?>" <?php \selected( $quote_interaction, ACTIVITYPUB_INTERACTION_POLICY_ME ); ?>><?php \esc_html_e( 'Just me', 'activitypub' ); ?></option>
|
||||
</select>
|
||||
<span class="howto">
|
||||
<?php \esc_html_e( 'Quoting allows others to cite your post while adding their own commentary.', 'activitypub' ); ?>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: The current site default quote policy. Note the leading space. */
|
||||
\esc_html__( ' Site default: %s', 'activitypub' ),
|
||||
\esc_html( self::get_quote_policy_label( $default_quote_policy ) )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label for a quote policy value.
|
||||
*
|
||||
* @param string $policy The policy value.
|
||||
*
|
||||
* @return string The translated label.
|
||||
*/
|
||||
private static function get_quote_policy_label( $policy ) {
|
||||
$labels = array(
|
||||
ACTIVITYPUB_INTERACTION_POLICY_ANYONE => \__( 'Anyone', 'activitypub' ),
|
||||
ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS => \__( 'Followers only', 'activitypub' ),
|
||||
ACTIVITYPUB_INTERACTION_POLICY_ME => \__( 'Just me', 'activitypub' ),
|
||||
);
|
||||
|
||||
return $labels[ $policy ] ?? $labels[ ACTIVITYPUB_INTERACTION_POLICY_ANYONE ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default visibility based on post age and federation status.
|
||||
*
|
||||
* @param \WP_Post $post The post object.
|
||||
*
|
||||
* @return string The default visibility value.
|
||||
*/
|
||||
private static function get_default_visibility( $post ) {
|
||||
// If already set, use that value.
|
||||
$saved_visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
||||
if ( $saved_visibility ) {
|
||||
return $saved_visibility;
|
||||
}
|
||||
|
||||
// If post is federated, use public.
|
||||
$status = \get_post_meta( $post->ID, 'activitypub_status', true );
|
||||
if ( ACTIVITYPUB_OBJECT_STATE_FEDERATED === $status ) {
|
||||
return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
// If post is older than 1 month, default to local.
|
||||
$post_timestamp = \strtotime( $post->post_date );
|
||||
$one_month_ago = \strtotime( '-30 days' );
|
||||
|
||||
if ( $post_timestamp < $one_month_ago ) {
|
||||
return ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL;
|
||||
}
|
||||
|
||||
// Default to public for new posts.
|
||||
return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save ActivityPub meta data.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*/
|
||||
public static function save_meta_data( $post_id ) {
|
||||
// Check if this is an autosave.
|
||||
if ( \defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process for post types that support ActivityPub.
|
||||
if ( ! \post_type_supports( \get_post_type( $post_id ), 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check user permissions.
|
||||
if ( ! \current_user_can( 'edit_post', $post_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify nonce is present and valid.
|
||||
if ( ! isset( $_POST['activitypub_meta_box_nonce'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['activitypub_meta_box_nonce'] ) ), 'activitypub_meta_box' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save content warning.
|
||||
if ( isset( $_POST['activitypub_content_warning'] ) ) {
|
||||
$content_warning = \sanitize_text_field( \wp_unslash( $_POST['activitypub_content_warning'] ) );
|
||||
if ( ! empty( $content_warning ) ) {
|
||||
\update_post_meta( $post_id, 'activitypub_content_warning', $content_warning );
|
||||
} else {
|
||||
\delete_post_meta( $post_id, 'activitypub_content_warning' );
|
||||
}
|
||||
}
|
||||
|
||||
// Save max image attachments.
|
||||
if ( isset( $_POST['activitypub_max_image_attachments'] ) ) {
|
||||
$max_images = \absint( $_POST['activitypub_max_image_attachments'] );
|
||||
\update_post_meta( $post_id, 'activitypub_max_image_attachments', $max_images );
|
||||
}
|
||||
|
||||
// Save content visibility.
|
||||
if ( isset( $_POST['activitypub_content_visibility'] ) ) {
|
||||
$visibility = \sanitize_text_field( \wp_unslash( $_POST['activitypub_content_visibility'] ) );
|
||||
\update_post_meta( $post_id, 'activitypub_content_visibility', $visibility );
|
||||
}
|
||||
|
||||
// Save quote interaction policy.
|
||||
if ( isset( $_POST['activitypub_interaction_policy_quote'] ) ) {
|
||||
$quote_policy = \sanitize_text_field( \wp_unslash( $_POST['activitypub_interaction_policy_quote'] ) );
|
||||
\update_post_meta( $post_id, 'activitypub_interaction_policy_quote', $quote_policy );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Jetpack integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Following;
|
||||
use Activitypub\Http;
|
||||
use Automattic\Jetpack\Connection\Manager;
|
||||
|
||||
use function Activitypub\is_activity_object;
|
||||
|
||||
/**
|
||||
* Jetpack integration class.
|
||||
*/
|
||||
class Jetpack {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! \defined( 'IS_WPCOM' ) ) {
|
||||
\add_filter( 'jetpack_sync_options_whitelist', array( self::class, 'add_sync_options' ) );
|
||||
\add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) );
|
||||
\add_filter( 'jetpack_sync_comment_meta_whitelist', array( self::class, 'add_sync_comment_meta' ) );
|
||||
\add_filter( 'jetpack_sync_whitelisted_comment_types', array( self::class, 'add_comment_types' ) );
|
||||
\add_filter( 'jetpack_json_api_comment_types', array( self::class, 'add_comment_types' ) );
|
||||
\add_filter( 'jetpack_api_include_comment_types_count', array( self::class, 'add_comment_types' ) );
|
||||
}
|
||||
|
||||
if (
|
||||
( \defined( 'IS_WPCOM' ) && IS_WPCOM ) ||
|
||||
( \class_exists( '\Automattic\Jetpack\Connection\Manager' ) && ( new Manager() )->is_user_connected() )
|
||||
) {
|
||||
\add_filter( 'activitypub_following_row_actions', array( self::class, 'add_reader_link' ), 10, 2 );
|
||||
\add_filter( 'pre_option_activitypub_following_ui', array( self::class, 'pre_option_activitypub_following_ui' ) );
|
||||
}
|
||||
|
||||
\add_action( 'load-post-new.php', array( self::class, 'adapt_post_share' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub options to the Jetpack sync allow list.
|
||||
*
|
||||
* @since 8.1.0
|
||||
*
|
||||
* @param array $allow_list The Jetpack sync options allow list.
|
||||
*
|
||||
* @return array The allow list with ActivityPub options.
|
||||
*/
|
||||
public static function add_sync_options( $allow_list ) {
|
||||
$allow_list[] = 'activitypub_blog_identifier';
|
||||
$allow_list[] = 'activitypub_blog_description';
|
||||
$allow_list[] = 'activitypub_header_image';
|
||||
$allow_list[] = 'activitypub_actor_mode';
|
||||
|
||||
return $allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub meta keys to the Jetpack sync allow list.
|
||||
*
|
||||
* @param array $allow_list The Jetpack sync allow list.
|
||||
*
|
||||
* @return array The Jetpack sync allow list with ActivityPub meta keys.
|
||||
*/
|
||||
public static function add_sync_meta( $allow_list ) {
|
||||
$allow_list[] = Followers::FOLLOWER_META_KEY;
|
||||
$allow_list[] = Following::FOLLOWING_META_KEY;
|
||||
|
||||
return $allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub comment meta keys to the Jetpack sync allow list.
|
||||
*
|
||||
* @param array $allow_list The Jetpack sync allow list.
|
||||
*
|
||||
* @return array The Jetpack sync allow list with ActivityPub comment meta keys.
|
||||
*/
|
||||
public static function add_sync_comment_meta( $allow_list ) {
|
||||
$allow_list[] = 'avatar_url';
|
||||
|
||||
return $allow_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom comment types to the list of comment types.
|
||||
*
|
||||
* @param array $comment_types Default comment types.
|
||||
*
|
||||
* @return array The comment types with ActivityPub types added.
|
||||
*/
|
||||
public static function add_comment_types( $comment_types ) {
|
||||
$comment_types[] = 'like';
|
||||
$comment_types[] = 'quote';
|
||||
$comment_types[] = 'repost';
|
||||
|
||||
return array_unique( $comment_types );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a "Reader" link to the bulk actions dropdown on the following list screen.
|
||||
*
|
||||
* @param array $actions The bulk actions.
|
||||
* @param array $item The current following item.
|
||||
*
|
||||
* @return array The bulk actions with the "Reader" link.
|
||||
*/
|
||||
public static function add_reader_link( $actions, $item ) {
|
||||
// Do not show the link for pending follow requests.
|
||||
if ( 'pending' === $item['status'] ) {
|
||||
return $actions;
|
||||
}
|
||||
|
||||
$feed = \get_post_meta( $item['id'], '_activitypub_actor_feed', true );
|
||||
|
||||
// Generate Reader URL based on environment.
|
||||
if ( \defined( 'IS_WPCOM' ) && IS_WPCOM ) {
|
||||
if ( empty( $feed['feed_id'] ) ) {
|
||||
return $actions; // No feed_id available on WPCOM.
|
||||
}
|
||||
$url = sprintf( 'https://wordpress.com/reader/feeds/%d', (int) $feed['feed_id'] );
|
||||
} else {
|
||||
$url = sprintf( 'https://wordpress.com/reader/feeds/lookup/%s', rawurlencode( $item['identifier'] ) );
|
||||
}
|
||||
|
||||
return array_merge(
|
||||
array(
|
||||
'reader' => sprintf(
|
||||
'<a href="%1$s" target="_blank">%2$s<span class="screen-reader-text"> %3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a>',
|
||||
esc_url( $url ),
|
||||
esc_html__( 'View Feed', 'activitypub' ),
|
||||
/* translators: Hidden accessibility text. */
|
||||
esc_html__( '(opens in a new tab)', 'activitypub' )
|
||||
),
|
||||
),
|
||||
$actions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force the ActivityPub Following UI to be enabled when Jetpack is active.
|
||||
*
|
||||
* @return string '1' to enable the ActivityPub Following UI.
|
||||
*/
|
||||
public static function pre_option_activitypub_following_ui() {
|
||||
return '1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the parameters for a post share request to be compatible with the Federated Reply block.
|
||||
*/
|
||||
public static function adapt_post_share() {
|
||||
if ( ! isset( $_GET['is_post_share'], $_GET['url'] ) || ! $_GET['is_post_share'] ) { // phpcs:ignore WordPress.Security
|
||||
return;
|
||||
}
|
||||
|
||||
$url = \sanitize_url( \wp_unslash( $_GET['url'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
|
||||
|
||||
if ( is_activity_object( Http::get_remote_object( $url ) ) ) {
|
||||
$args = array(
|
||||
'post_type' => 'post',
|
||||
'in_reply_to' => $url,
|
||||
);
|
||||
|
||||
\wp_safe_redirect( \add_query_arg( $args, \admin_url( 'post-new.php' ) ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/**
|
||||
* LiteSpeed Cache integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\is_plugin_active;
|
||||
|
||||
/**
|
||||
* LiteSpeed Cache integration.
|
||||
*
|
||||
* @see https://wordpress.org/support/topic/avoiding-caching-activitypub-content/
|
||||
*/
|
||||
class Litespeed_Cache {
|
||||
|
||||
/**
|
||||
* The rules to add to the htaccess file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $rules = '<IfModule LiteSpeed>
|
||||
RewriteEngine On
|
||||
RewriteCond %{HTTP:Accept} application
|
||||
RewriteRule ^ - [E=Cache-Control:vary=%{ENV:LSCACHE_VARY_VALUE}+isjson]
|
||||
</IfModule>';
|
||||
|
||||
/**
|
||||
* The option name to store the htaccess rules.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $option_name = 'activitypub_litespeed_cache_setup';
|
||||
|
||||
/**
|
||||
* The marker to identify the rules in the htaccess file.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $marker = 'ActivityPub LiteSpeed Cache';
|
||||
|
||||
/**
|
||||
* The LiteSpeed Cache plugin slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $plugin_slug = 'litespeed-cache/litespeed-cache.php';
|
||||
|
||||
/**
|
||||
* Initialize the integration.
|
||||
*/
|
||||
public static function init() {
|
||||
// Add rules if LiteSpeed Cache is active and rules aren't set.
|
||||
if ( is_plugin_active( self::$plugin_slug ) ) {
|
||||
if ( ! \get_option( self::$option_name ) ) {
|
||||
self::add_htaccess_rules();
|
||||
}
|
||||
|
||||
\add_filter( 'site_status_tests', array( self::class, 'add_site_health_test' ) );
|
||||
|
||||
// Remove rules if LiteSpeed Cache is not active but rules were previously set.
|
||||
} elseif ( \get_option( self::$option_name ) ) {
|
||||
self::remove_htaccess_rules();
|
||||
}
|
||||
|
||||
// Clean up when LiteSpeed Cache plugin is deleted.
|
||||
\add_action( 'deleted_plugin', array( self::class, 'on_plugin_deleted' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up htaccess rules when LiteSpeed Cache plugin is deleted.
|
||||
*
|
||||
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
|
||||
*/
|
||||
public static function on_plugin_deleted( $plugin_file ) {
|
||||
if ( self::$plugin_slug === $plugin_file && \get_option( self::$option_name ) ) {
|
||||
self::remove_htaccess_rules();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the LiteSpeed Cache htaccess rules.
|
||||
*/
|
||||
public static function add_htaccess_rules() {
|
||||
$added_rules = self::append_with_markers( self::$marker, self::$rules );
|
||||
|
||||
if ( $added_rules ) {
|
||||
\update_option( self::$option_name, '1' );
|
||||
} else {
|
||||
\update_option( self::$option_name, '0' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the LiteSpeed Cache htaccess rules.
|
||||
*/
|
||||
public static function remove_htaccess_rules() {
|
||||
self::append_with_markers( self::$marker, '' );
|
||||
|
||||
\delete_option( self::$option_name );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the LiteSpeed Cache config test to site health.
|
||||
*
|
||||
* @param array $tests The site health tests.
|
||||
*
|
||||
* @return array The site health tests with the LiteSpeed Cache config test.
|
||||
*/
|
||||
public static function add_site_health_test( $tests ) {
|
||||
$tests['direct']['activitypub_test_litespeed_cache_integration'] = array(
|
||||
'label' => \__( 'LiteSpeed Cache Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_litespeed_cache_integration' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the LiteSpeed Cache integration.
|
||||
*
|
||||
* @return array The test results.
|
||||
*/
|
||||
public static function test_litespeed_cache_integration() {
|
||||
$result = array(
|
||||
'label' => \__( 'Compatibility with LiteSpeed Cache', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'LiteSpeed Cache is well configured to work with ActivityPub.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_litespeed_cache_integration',
|
||||
);
|
||||
|
||||
if ( ! \get_option( self::$option_name ) ) {
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'LiteSpeed Cache might not be properly configured.', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'LiteSpeed Cache isn’t currently set up to work with ActivityPub. While this isn’t a major problem, it’s a good idea to enable support. Without it, some technical files (like JSON) might accidentally show up in your website’s cache and be visible to visitors.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] = \sprintf(
|
||||
'<p>%s</p><pre>%s</pre>',
|
||||
\__( 'To enable the ActivityPub integration with LiteSpeed Cache, add the following rules to your <code>.htaccess</code> file:', 'activitypub' ),
|
||||
\esc_html( self::$rules )
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend rules to the top of a file with markers.
|
||||
*
|
||||
* @param string $marker The marker to identify the rules in the file.
|
||||
* @param string $rules The rules to prepend.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
private static function append_with_markers( $marker, $rules ) {
|
||||
$htaccess_file = self::get_htaccess_file_path();
|
||||
|
||||
if ( ! \wp_is_writable( $htaccess_file ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure WP_Filesystem() is declared.
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
||||
global $wp_filesystem;
|
||||
\WP_Filesystem();
|
||||
|
||||
$htaccess = $wp_filesystem->get_contents( $htaccess_file );
|
||||
|
||||
// If marker exists, remove the old block first.
|
||||
if ( strpos( $htaccess, $marker ) !== false ) {
|
||||
// Remove existing marker block.
|
||||
$pattern = '/# BEGIN ' . preg_quote( $marker, '/' ) . '.*?# END ' . preg_quote( $marker, '/' ) . '\r?\n?/s';
|
||||
$htaccess = preg_replace( $pattern, '', $htaccess );
|
||||
$htaccess = trim( $htaccess );
|
||||
}
|
||||
|
||||
// If rules are empty, just return (for removal case).
|
||||
if ( empty( $rules ) ) {
|
||||
return $wp_filesystem->put_contents( $htaccess_file, $htaccess, FS_CHMOD_FILE );
|
||||
}
|
||||
|
||||
// Prepend new rules to the top of the file.
|
||||
$start_marker = "# BEGIN {$marker}";
|
||||
$end_marker = "# END {$marker}";
|
||||
|
||||
$rules = $start_marker . PHP_EOL . $rules . PHP_EOL . $end_marker;
|
||||
$htaccess = $rules . PHP_EOL . PHP_EOL . $htaccess;
|
||||
|
||||
return $wp_filesystem->put_contents( $htaccess_file, $htaccess, FS_CHMOD_FILE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the htaccess file.
|
||||
*
|
||||
* @return string|false The htaccess file or false.
|
||||
*/
|
||||
private static function get_htaccess_file_path() {
|
||||
$htaccess_file = false;
|
||||
|
||||
// Ensure get_home_path() is declared.
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
if ( @file_exists( \get_home_path() . '.htaccess' ) ) {
|
||||
/** The htaccess file resides in ABSPATH */
|
||||
$htaccess_file = \get_home_path() . '.htaccess';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the htaccess file path.
|
||||
*
|
||||
* @param string|false $htaccess_file The htaccess file path.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_litespeed_cache_htaccess_file', $htaccess_file );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Multisite Language Switcher integration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* Compatibility with the Multisite Language Switcher plugin.
|
||||
*
|
||||
* @see https://github.com/lloc/Multisite-Language-Switcher/
|
||||
*/
|
||||
class Multisite_Language_Switcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'save_post', array( self::class, 'ignore_outbox_post' ), 9, 2 );
|
||||
\add_action( 'save_post', array( self::class, 'unignore_outbox_post' ), 11, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Short-circuit saving Multisite Language Switcher data for the Outbox post type.
|
||||
*
|
||||
* @param int $post_id The post id.
|
||||
* @param \WP_Post $post The post object.
|
||||
*/
|
||||
public static function ignore_outbox_post( $post_id, $post ) {
|
||||
if ( Outbox::POST_TYPE === $post->post_type ) {
|
||||
\add_action( 'msls_main_save', '__return_null' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove short-circuit for Multisite Language Switcher data.
|
||||
*
|
||||
* @param int $post_id The post id.
|
||||
* @param \WP_Post $post The post object.
|
||||
*/
|
||||
public static function unignore_outbox_post( $post_id, $post ) {
|
||||
if ( Outbox::POST_TYPE === $post->post_type ) {
|
||||
\remove_action( 'msls_main_save', '__return_null' );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* NodeInfo integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Webfinger;
|
||||
|
||||
use function Activitypub\get_active_users;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_total_users;
|
||||
|
||||
/**
|
||||
* Compatibility with the NodeInfo plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/nodeinfo/
|
||||
*/
|
||||
class Nodeinfo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 );
|
||||
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ) );
|
||||
|
||||
\add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo data.
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo data.
|
||||
* @param string $version The NodeInfo Version.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_nodeinfo_data( $nodeinfo, $version ) {
|
||||
$nodeinfo = wp_parse_args(
|
||||
$nodeinfo,
|
||||
array(
|
||||
'version' => $version,
|
||||
'software' => array(),
|
||||
'usage' => array(
|
||||
'users' => array(
|
||||
'total' => 0,
|
||||
'activeMonth' => 0,
|
||||
'activeHalfyear' => 0,
|
||||
),
|
||||
),
|
||||
'protocols' => array(),
|
||||
'services' => array(
|
||||
'inbound' => array(),
|
||||
'outbound' => array(),
|
||||
),
|
||||
'metadata' => array(),
|
||||
)
|
||||
);
|
||||
|
||||
if ( \version_compare( $version, '2.1', '>=' ) ) {
|
||||
$nodeinfo['software']['homepage'] = 'https://wordpress.org/plugins/activitypub/';
|
||||
$nodeinfo['software']['repository'] = 'https://github.com/Automattic/wordpress-activitypub';
|
||||
}
|
||||
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
$nodeinfo['services']['inbound'] = array_merge(
|
||||
$nodeinfo['services']['inbound'],
|
||||
array( 'gnusocial' )
|
||||
);
|
||||
$nodeinfo['services']['outbound'] = array_merge(
|
||||
$nodeinfo['services']['outbound'],
|
||||
array( 'friendica', 'gnusocial', 'mediagoblin', 'wordpress' )
|
||||
);
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users(),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
);
|
||||
|
||||
$nodeinfo['metadata']['federation'] = array( 'enabled' => true );
|
||||
$nodeinfo['metadata']['staffAccounts'] = self::get_staff();
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo2 data.
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo2 data.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_nodeinfo2_data( $nodeinfo ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users(),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the well-known nodeinfo data.
|
||||
*
|
||||
* @param array $data The well-known nodeinfo data.
|
||||
*
|
||||
* @return array The extended array.
|
||||
*/
|
||||
public static function add_wellknown_nodeinfo_data( $data ) {
|
||||
$data['links'][] = array(
|
||||
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||
'href' => get_rest_url_by_path( 'application' ),
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all staff accounts (admin users with the "activitypub" capability) and return them in WebFinger resource format.
|
||||
*
|
||||
* @return array List of staff accounts in WebFinger resource format.
|
||||
*/
|
||||
private static function get_staff() {
|
||||
// Get all admin users with the cap activitypub.
|
||||
$admins = get_users(
|
||||
array(
|
||||
'role' => 'administrator',
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'cap' => 'activitypub',
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
$admins = array_map( array( Webfinger::class, 'get_user_resource' ), $admins );
|
||||
|
||||
return array_values( array_filter( $admins ) );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Opengraph integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Model\Blog;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Compatibility with the OpenGraph plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/opengraph/
|
||||
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md
|
||||
* @see https://github.com/mastodon/mastodon/pull/30398
|
||||
*/
|
||||
class Opengraph {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! function_exists( 'opengraph_metadata' ) ) {
|
||||
\add_action( 'wp_head', array( self::class, 'add_meta_tags' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'opengraph_metadata', array( self::class, 'add_metadata' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub prefix to the OpenGraph prefixes.
|
||||
*
|
||||
* @param array $prefixes the current prefixes.
|
||||
*
|
||||
* @return array the updated prefixes.
|
||||
*/
|
||||
public static function add_prefixes( $prefixes ) {
|
||||
// @todo discuss namespace
|
||||
$prefixes['fediverse'] = 'https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md';
|
||||
|
||||
return $prefixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub metadata to the OpenGraph metadata.
|
||||
*
|
||||
* @param array $metadata the current metadata.
|
||||
*
|
||||
* @return array the updated metadata.
|
||||
*/
|
||||
public static function add_metadata( $metadata ) {
|
||||
// Always show Blog-User if the Blog is in single user mode.
|
||||
if ( is_single_user() ) {
|
||||
$user = new Blog();
|
||||
|
||||
// Add WebFinger resource.
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
// Use the Author of the Archive-Page.
|
||||
$user_id = \get_queried_object_id();
|
||||
} elseif ( \is_singular() ) {
|
||||
// Use the Author of the Post.
|
||||
$user_id = \get_post_field( 'post_author', \get_queried_object_id() );
|
||||
} elseif ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
// Use the Blog-User for any other page, if the Blog-User is not disabled.
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
} else {
|
||||
// Do not add any metadata otherwise.
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
|
||||
if ( ! $user || \is_wp_error( $user ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
// Add WebFinger resource.
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Open Graph <meta> tags in the page header.
|
||||
*/
|
||||
public static function add_meta_tags() {
|
||||
$metadata = apply_filters( 'opengraph_metadata', array() );
|
||||
foreach ( $metadata as $key => $value ) {
|
||||
if ( empty( $key ) || empty( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
$value = (array) $value;
|
||||
|
||||
foreach ( $value as $v ) {
|
||||
printf(
|
||||
'<meta property="%1$s" name="%1$s" content="%2$s" />' . PHP_EOL,
|
||||
esc_attr( $key ),
|
||||
esc_attr( $v )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,180 @@
|
||||
<?php
|
||||
/**
|
||||
* Podlove Podcast Publisher integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\seconds_to_iso8601;
|
||||
|
||||
/**
|
||||
* Compatibility with the Podlove Podcast Publisher plugin.
|
||||
*
|
||||
* This is a transformer for the Podlove Podcast Publisher plugin,
|
||||
* that extends the default transformer for WordPress posts.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/podlove-podcasting-plugin-for-wordpress/
|
||||
*/
|
||||
class Podlove_Podcast_Publisher extends Post {
|
||||
/**
|
||||
* The Podlove Episode object.
|
||||
*
|
||||
* @var \Podlove\Model\Episode|null
|
||||
*/
|
||||
private $episode = null;
|
||||
|
||||
/**
|
||||
* Get the Podlove Episode object.
|
||||
*
|
||||
* @return \Podlove\Model\Episode|null The episode object or null if not found.
|
||||
*/
|
||||
protected function get_episode() {
|
||||
if ( null === $this->episode && \class_exists( '\Podlove\Model\Episode' ) ) {
|
||||
$this->episode = \Podlove\Model\Episode::find_one_by_post_id( $this->item->ID );
|
||||
}
|
||||
return $this->episode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attachment for a podcast episode.
|
||||
*
|
||||
* This method is overridden to add the audio/video files as attachments.
|
||||
*
|
||||
* @return array The attachments array.
|
||||
*/
|
||||
public function get_attachment() {
|
||||
$episode = $this->get_episode();
|
||||
|
||||
if ( ! $episode ) {
|
||||
return parent::get_attachment();
|
||||
}
|
||||
|
||||
$attachments = array();
|
||||
|
||||
// Get media files from Podlove.
|
||||
$media_files = $episode->media_files();
|
||||
|
||||
foreach ( $media_files as $media_file ) {
|
||||
if ( ! $media_file->is_valid() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$episode_asset = $media_file->episode_asset();
|
||||
|
||||
if ( ! $episode_asset ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_type = $episode_asset->file_type();
|
||||
|
||||
if ( ! $file_type ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only include audio and video files.
|
||||
if ( ! in_array( $file_type->type, array( 'audio', 'video' ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use tracking URL if analytics is enabled, otherwise direct file URL.
|
||||
if ( 'ptm_analytics' === \Podlove\get_setting( 'tracking', 'mode' ) ) {
|
||||
$file_url = $media_file->get_public_file_url( 'activitypub' );
|
||||
} else {
|
||||
$file_url = $media_file->get_file_url();
|
||||
}
|
||||
|
||||
$attachment = array(
|
||||
'type' => \esc_attr( ucfirst( $file_type->type ) ),
|
||||
'url' => \esc_url( $file_url ),
|
||||
'mediaType' => \esc_attr( $file_type->mime_type ),
|
||||
'name' => \esc_attr( $episode->title() ?? '' ),
|
||||
);
|
||||
|
||||
// Add duration if available (in ISO 8601 format).
|
||||
$duration = $episode->get_duration( 'seconds' );
|
||||
if ( $duration && is_numeric( $duration ) && (int) $duration > 0 ) {
|
||||
$attachment['duration'] = seconds_to_iso8601( (int) $duration );
|
||||
}
|
||||
|
||||
$attachments[] = $attachment;
|
||||
}
|
||||
|
||||
// If we have media files, add episode image as icon.
|
||||
if ( ! empty( $attachments ) ) {
|
||||
$icon = $this->get_episode_image();
|
||||
|
||||
if ( $icon ) {
|
||||
foreach ( $attachments as $key => $attachment ) {
|
||||
$attachments[ $key ]['icon'] = \esc_url( $icon );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no Podlove media files found, fall back to parent.
|
||||
if ( empty( $attachments ) ) {
|
||||
return parent::get_attachment();
|
||||
}
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the episode image URL.
|
||||
*
|
||||
* @return string|null The image URL or null if not found.
|
||||
*/
|
||||
protected function get_episode_image() {
|
||||
$episode = $this->get_episode();
|
||||
|
||||
if ( ! $episode ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$image = $episode->cover_art_with_fallback();
|
||||
|
||||
if ( $image && method_exists( $image, 'url' ) ) {
|
||||
return $image->url();
|
||||
}
|
||||
|
||||
// Fall back to post thumbnail.
|
||||
$icon = $this->get_icon();
|
||||
if ( $icon ) {
|
||||
return object_to_uri( $icon );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the duration of the episode in ISO 8601 format.
|
||||
*
|
||||
* @return string|null The duration in ISO 8601 format or null if not available.
|
||||
*/
|
||||
public function get_duration() {
|
||||
$episode = $this->get_episode();
|
||||
|
||||
if ( ! $episode ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$duration_seconds = $episode->get_duration( 'seconds' );
|
||||
|
||||
// Ensure we have a valid numeric duration.
|
||||
if ( ! $duration_seconds || ! is_numeric( $duration_seconds ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$duration_seconds = (int) $duration_seconds;
|
||||
|
||||
if ( $duration_seconds <= 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return seconds_to_iso8601( $duration_seconds );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Seriously Simple Podcasting integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Compatibility with the Seriously Simple Podcasting plugin.
|
||||
*
|
||||
* This is a transformer for the Seriously Simple Podcasting plugin,
|
||||
* that extends the default transformer for WordPress posts.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
|
||||
*/
|
||||
class Seriously_Simple_Podcasting extends Post {
|
||||
/**
|
||||
* Gets the attachment for a podcast episode.
|
||||
*
|
||||
* This method is overridden to add the audio file as an attachment.
|
||||
*
|
||||
* @return array The attachments array.
|
||||
*/
|
||||
public function get_attachment() {
|
||||
$post = $this->item;
|
||||
$attachment = array(
|
||||
'type' => \esc_attr( ucfirst( \get_post_meta( $post->ID, 'episode_type', true ) ?? 'Audio' ) ),
|
||||
'url' => \esc_url( \get_post_meta( $post->ID, 'audio_file', true ) ),
|
||||
'name' => \esc_attr( \get_the_title( $post->ID ) ?? '' ),
|
||||
);
|
||||
|
||||
$icon = \get_post_meta( $post->ID, 'cover_image', true );
|
||||
if ( ! $icon ) {
|
||||
$icon = $this->get_icon();
|
||||
}
|
||||
|
||||
if ( $icon ) {
|
||||
$attachment['icon'] = \esc_url( object_to_uri( $icon ) );
|
||||
}
|
||||
|
||||
return array( $attachment );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
/**
|
||||
* Surge integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\is_plugin_active;
|
||||
|
||||
/**
|
||||
* Surge Cache integration.
|
||||
*
|
||||
* This class handles the compatibility with the Surge plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/surge/
|
||||
*/
|
||||
class Surge {
|
||||
|
||||
/**
|
||||
* The pattern to find the Surge cache config constant.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public static $cache_config_pattern = '/define\s*\(\s*[\'"](WP_CACHE_CONFIG)[\'"]\s*,\s*[\'"](.*?)[\'"]\s*\)\s*;/i';
|
||||
|
||||
/**
|
||||
* Initialize the Surge integration.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activate_surge/surge.php', array( self::class, 'add_cache_config' ) );
|
||||
\add_action( 'deactivate_surge/surge.php', array( self::class, 'remove_cache_config' ) );
|
||||
|
||||
\add_filter( 'site_status_tests', array( self::class, 'maybe_add_site_health' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Surge cache config.
|
||||
*/
|
||||
public static function add_cache_config() {
|
||||
// Check if surge is installed and active.
|
||||
if ( ! is_plugin_active( 'surge/surge.php' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the constant already exists.
|
||||
if ( \defined( 'WP_CACHE_CONFIG' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = self::get_config_file_path();
|
||||
|
||||
if ( ! \wp_is_writable( $file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \function_exists( 'WP_Filesystem' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
\WP_Filesystem();
|
||||
|
||||
$config = $wp_filesystem->get_contents( $file );
|
||||
|
||||
// Check if the constant already exists.
|
||||
if ( \preg_match( self::$cache_config_pattern, $config ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a WP_CACHE_CONFIG to wp-config.php.
|
||||
$anchor = "/* That's all, stop editing!";
|
||||
if ( false !== \strpos( $config, $anchor ) ) {
|
||||
$config = \str_replace( $anchor, self::get_cache_config() . PHP_EOL . PHP_EOL . $anchor, $config );
|
||||
} elseif ( false !== \strpos( $config, '<?php' ) ) {
|
||||
$config = \str_replace( '<?php', '<?php' . PHP_EOL . PHP_EOL . self::get_cache_config() . PHP_EOL, $config );
|
||||
}
|
||||
|
||||
$wp_filesystem->put_contents( $file, $config, FS_CHMOD_FILE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Surge cache config.
|
||||
*/
|
||||
public static function remove_cache_config() {
|
||||
if ( ! \defined( 'WP_CACHE_CONFIG' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$file = self::get_config_file_path();
|
||||
|
||||
if ( ! \wp_is_writable( $file ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
global $wp_filesystem;
|
||||
\WP_Filesystem();
|
||||
|
||||
$config = $wp_filesystem->get_contents( $file );
|
||||
|
||||
// Remove the define line.
|
||||
$config = \preg_replace( PHP_EOL . self::$cache_config_pattern . PHP_EOL, '', $config );
|
||||
|
||||
$wp_filesystem->put_contents( $file, $config, FS_CHMOD_FILE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config file.
|
||||
*
|
||||
* @return string|false The config file or false.
|
||||
*/
|
||||
public static function get_config_file_path() {
|
||||
$config_file = false;
|
||||
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
if ( @file_exists( ABSPATH . 'wp-config.php' ) ) {
|
||||
|
||||
/** The config file resides in ABSPATH */
|
||||
$config_file = ABSPATH . 'wp-config.php';
|
||||
// phpcs:ignore WordPress.PHP.NoSilencedErrors
|
||||
} elseif ( @file_exists( dirname( ABSPATH ) . '/wp-config.php' ) && ! @file_exists( dirname( ABSPATH ) . '/wp-settings.php' ) ) {
|
||||
|
||||
/** The config file resides one level above ABSPATH but is not part of another installation */
|
||||
$config_file = dirname( ABSPATH ) . '/wp-config.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the config file path.
|
||||
*
|
||||
* @param string|false $config_file The config file path.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_surge_cache_config_file', $config_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add the Surge cache config to the site health.
|
||||
*
|
||||
* @param array $tests The site health tests.
|
||||
*
|
||||
* @return array The site health tests with the Surge cache config test.
|
||||
*/
|
||||
public static function maybe_add_site_health( $tests ) {
|
||||
if ( ! is_plugin_active( 'surge/surge.php' ) ) {
|
||||
return $tests;
|
||||
}
|
||||
|
||||
$tests['direct']['activitypub_test_surge_integration'] = array(
|
||||
'label' => \__( 'Surge Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_surge_integration' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Surge integration test.
|
||||
*
|
||||
* @return array The test result.
|
||||
*/
|
||||
public static function test_surge_integration() {
|
||||
$result = array(
|
||||
'label' => \__( 'Compatibility with Surge', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Surge is well configured to work with ActivityPub.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_surge_integration',
|
||||
);
|
||||
|
||||
if ( ! \defined( 'WP_CACHE_CONFIG' ) ) {
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Surge might not be properly configured.', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Surge isn’t currently set up to work with ActivityPub. While this isn’t a major problem, it’s a good idea to enable support. Without it, some technical files (like JSON) might accidentally show up in your website’s cache and be visible to visitors.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\sprintf(
|
||||
// translators: %s: Plugin directory path.
|
||||
\__( 'To enable the ActivityPub integration with Surge, add the following line to your <code>wp-config.php</code> file: <br /><code>%s</code>', 'activitypub' ),
|
||||
self::get_cache_config()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache config.
|
||||
*
|
||||
* @return string The cache config.
|
||||
*/
|
||||
public static function get_cache_config() {
|
||||
return \sprintf( "define( 'WP_CACHE_CONFIG', '%s/integration/surge-cache-config.php' );", \rtrim( ACTIVITYPUB_PLUGIN_DIR, '/' ) );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* WebFinger integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Compatibility with the WebFinger plugin
|
||||
*
|
||||
* @see https://wordpress.org/plugins/webfinger/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 );
|
||||
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 1, 2 );
|
||||
|
||||
\add_filter( 'webfinger_data', array( self::class, 'add_interaction_links' ), 11 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links.
|
||||
*
|
||||
* @param array $jrd The jrd array.
|
||||
* @param string $uri The WebFinger resource.
|
||||
* @param \WP_User $user The WordPress user.
|
||||
*
|
||||
* @return array The jrd array.
|
||||
*/
|
||||
public static function add_user_discovery( $jrd, $uri, $user ) {
|
||||
$user = Actors::get_by_id( $user->ID );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $jrd;
|
||||
}
|
||||
|
||||
$jrd['subject'] = sprintf( 'acct:%s', $user->get_webfinger() );
|
||||
|
||||
$jrd['aliases'][] = $user->get_id();
|
||||
$jrd['aliases'][] = $user->get_url();
|
||||
$jrd['aliases'][] = $user->get_alternate_url();
|
||||
$jrd['aliases'] = array_unique( $jrd['aliases'] );
|
||||
$jrd['aliases'] = array_values( $jrd['aliases'] );
|
||||
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->get_id(),
|
||||
);
|
||||
|
||||
return $jrd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links.
|
||||
*
|
||||
* @param array $jrd The jrd array.
|
||||
* @param string $uri The WebFinger resource.
|
||||
*
|
||||
* @return array|\WP_Error The jrd array or WP_Error.
|
||||
*/
|
||||
public static function add_pseudo_user_discovery( $jrd, $uri ) {
|
||||
$user = Actors::get_by_resource( $uri );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$aliases = array(
|
||||
$user->get_id(),
|
||||
$user->get_url(),
|
||||
$user->get_alternate_url(),
|
||||
);
|
||||
|
||||
$aliases = array_unique( $aliases );
|
||||
$aliases = array_values( $aliases );
|
||||
|
||||
$profile = array(
|
||||
'subject' => sprintf( 'acct:%s', $user->get_webfinger() ),
|
||||
'aliases' => $aliases,
|
||||
'links' => array(
|
||||
array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->get_id(),
|
||||
),
|
||||
array(
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => $user->get_id(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( 'Person' !== $user->get_type() ) {
|
||||
$profile['links'][0]['properties'] = array(
|
||||
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
|
||||
);
|
||||
}
|
||||
|
||||
return $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add interaction links to the WebFinger data.
|
||||
*
|
||||
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/3b86/fep-3b86.md
|
||||
*
|
||||
* @param array $jrd The jrd array.
|
||||
*
|
||||
* @return array The jrd array.
|
||||
*/
|
||||
public static function add_interaction_links( $jrd ) {
|
||||
if ( ! is_array( $jrd ) ) {
|
||||
return $jrd;
|
||||
}
|
||||
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||
'template' => get_rest_url_by_path( 'interactions?uri={uri}' ),
|
||||
);
|
||||
|
||||
/*
|
||||
* Note: The parameter name `{inReplyTo}` is used here for all 'Create' intents,
|
||||
* not just replies, to maintain compatibility with existing implementations and
|
||||
* the FEP-3b86 specification. If a more generic parameter name is adopted in the
|
||||
* future, this should be updated accordingly.
|
||||
*/
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'https://w3id.org/fep/3b86/Create',
|
||||
'template' => get_rest_url_by_path( 'interactions?uri={inReplyTo}&intent=create' ),
|
||||
);
|
||||
|
||||
$jrd['links'][] = array(
|
||||
'rel' => 'https://w3id.org/fep/3b86/Follow',
|
||||
'template' => get_rest_url_by_path( 'interactions?uri={object}&intent=follow' ),
|
||||
);
|
||||
|
||||
return $jrd;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* WP REST Cache integration file.
|
||||
*
|
||||
* This file contains code for caching ActivityPub REST requests.
|
||||
*
|
||||
* Copyright (C) 2025 Epiphyt
|
||||
* Original code: https://epiph.yt/en/blog/2025/accidental-ddos-through-activitypub-plugin/
|
||||
*
|
||||
* Portions of this code are adapted from GPL v2 licensed code.
|
||||
* As such, you may also redistribute and/or modify those portions under the terms of
|
||||
* the GNU General Public License as published by the Free Software Foundation.
|
||||
*
|
||||
* https://www.gnu.org/licenses/gpl-2.0.html
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Comment;
|
||||
use WP_Rest_Cache_Plugin\Includes\Caching\Caching;
|
||||
|
||||
/**
|
||||
* Compatibility with the WP REST Cache plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/wp-rest-cache/
|
||||
*/
|
||||
class WP_Rest_Cache {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'wp_rest_cache/allowed_endpoints', array( self::class, 'add_activitypub_endpoints' ) );
|
||||
\add_filter( 'wp_rest_cache/determine_object_type', array( self::class, 'set_object_type' ), 10, 4 );
|
||||
\add_filter( 'wp_rest_cache/is_single_item', array( self::class, 'set_is_single_item' ), 10, 3 );
|
||||
\add_action( 'transition_post_status', array( self::class, 'transition_post_status' ), 10, 3 );
|
||||
\add_action( 'transition_comment_status', array( self::class, 'transition_comment_status' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub endpoints to the list of allowed endpoints.
|
||||
*
|
||||
* @param array $endpoints List of allowed endpoints.
|
||||
*
|
||||
* @return array Filtered list of allowed endpoints.
|
||||
*/
|
||||
public static function add_activitypub_endpoints( $endpoints ) {
|
||||
$endpoints[ ACTIVITYPUB_REST_NAMESPACE ] = array( 'actors', 'collections', 'comments', 'interactions', 'nodeinfo', 'posts', 'users' );
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the cache represents a single item.
|
||||
*
|
||||
* Always return false for ActivityPub endpoints, since cache entries cannot be flushed otherwise.
|
||||
*
|
||||
* @param bool $is_single Whether the current cache represents a single item.
|
||||
* @param mixed $data Data to cache.
|
||||
* @param string $uri Request URI.
|
||||
*
|
||||
* @return bool Whether the cache represents a single item.
|
||||
*/
|
||||
public static function set_is_single_item( $is_single, $data, $uri ) {
|
||||
if ( self::is_activitypub_endpoint( $uri ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $is_single;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set object type for ActivityPub.
|
||||
*
|
||||
* @param string $object_type Object type.
|
||||
* @param string $cache_key Object key.
|
||||
* @param mixed $data Data to cache.
|
||||
* @param string $uri Request URI.
|
||||
*
|
||||
* @return string Updated object type.
|
||||
*/
|
||||
public static function set_object_type( $object_type, $cache_key, $data, $uri ) {
|
||||
if ( self::is_activitypub_endpoint( $uri ) ) {
|
||||
return 'ActivityPub';
|
||||
}
|
||||
|
||||
return $object_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cache by transition post status.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Old post status.
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public static function transition_post_status( $new_status, $old_status, $post ) {
|
||||
if ( 'publish' !== $new_status && 'publish' !== $old_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array() );
|
||||
$post_types[] = Outbox::POST_TYPE;
|
||||
|
||||
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Caching::get_instance()->delete_object_type_caches( 'ActivityPub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset cache by transition comment status.
|
||||
*
|
||||
* @param string $new_status The new comment status.
|
||||
* @param string $old_status The old comment status.
|
||||
* @param \WP_Comment $comment Comment object.
|
||||
*/
|
||||
public static function transition_comment_status( $new_status, $old_status, $comment ) {
|
||||
if ( 'approved' !== $new_status && 'approved' !== $old_status ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$comment_types = Comment::get_comment_type_slugs();
|
||||
$comment_types[] = 'comment';
|
||||
|
||||
if ( ! \in_array( $comment->comment_type ?: 'comment', $comment_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Caching::get_instance()->delete_object_type_caches( 'ActivityPub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test, whether the current endpoint is an ActivityPub endpoint.
|
||||
*
|
||||
* @param string $uri URI to test.
|
||||
*
|
||||
* @return bool Whether the current endpoint is an ActivityPub endpoint.
|
||||
*/
|
||||
private static function is_activitypub_endpoint( $uri ) {
|
||||
$search = '/' . ACTIVITYPUB_REST_NAMESPACE . '/';
|
||||
|
||||
return \str_contains( $uri, $search ) || \str_contains( $uri, 'rest_route=' . \rawurlencode( $search ) );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* WPML integration.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Compatibility with the WPML Multilingual CMS plugin.
|
||||
*
|
||||
* @see https://wpml.org/
|
||||
*/
|
||||
class WPML {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_locale', array( self::class, 'get_wpml_post_locale' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the post locale from the WPML post data.
|
||||
*
|
||||
* @param string $lang The language code.
|
||||
* @param mixed $post The post object.
|
||||
*
|
||||
* @return string The modified language code.
|
||||
*/
|
||||
public static function get_wpml_post_locale( $lang, $post ) {
|
||||
if ( ! $post instanceof \WP_Post ) {
|
||||
return $lang;
|
||||
}
|
||||
|
||||
$language_details = apply_filters( 'wpml_post_language_details', null, $post->ID );
|
||||
|
||||
if ( is_array( $language_details ) && isset( $language_details['language_code'] ) ) {
|
||||
$lang = $language_details['language_code'];
|
||||
}
|
||||
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
/**
|
||||
* Yoast SEO integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Yoast SEO integration class.
|
||||
*/
|
||||
class Yoast_Seo {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'site_status_tests', array( self::class, 'add_site_health_tests' ), 11 ); // After Health_Check::add_tests().
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Yoast-specific site health tests.
|
||||
*
|
||||
* @param array $tests The site health tests array.
|
||||
*
|
||||
* @return array The modified tests array.
|
||||
*/
|
||||
public static function add_site_health_tests( $tests ) {
|
||||
// Only add the test if attachment post type is supported by ActivityPub.
|
||||
if ( self::is_attachment_supported() ) {
|
||||
$tests['direct']['activitypub_yoast_seo_media_pages'] = array(
|
||||
'label' => \__( 'Yoast SEO Media Pages Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_yoast_seo_media_pages' ),
|
||||
);
|
||||
}
|
||||
|
||||
$tests['direct']['activitypub_yoast_seo_author_archives'] = array(
|
||||
'label' => \__( 'Yoast SEO Author Archives Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_yoast_seo_author_archives' ),
|
||||
);
|
||||
|
||||
// Remove author URL test if author archives are disabled. There is no need to show this error twice.
|
||||
if ( self::is_author_archives_disabled() && isset( $tests['direct']['activitypub_test_author_url'] ) ) {
|
||||
unset( $tests['direct']['activitypub_test_author_url'] );
|
||||
}
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Yoast's "Enable media pages" setting is properly configured.
|
||||
*
|
||||
* @return array The test result.
|
||||
*/
|
||||
public static function test_yoast_seo_media_pages() {
|
||||
$result = array(
|
||||
'label' => \__( 'Yoast SEO media pages are enabled', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Media pages are enabled in Yoast SEO, which allows media attachments to be federated and interacted with through ActivityPub.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_yoast_seo_media_pages',
|
||||
);
|
||||
|
||||
if ( self::is_media_pages_disabled() ) {
|
||||
$result['status'] = 'recommended';
|
||||
$result['label'] = \__( 'Yoast SEO media pages should be enabled', 'activitypub' );
|
||||
$result['badge']['color'] = 'orange';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Yoast SEO’s “Enable media pages” setting is currently disabled. Since you have media attachments configured to be federated through ActivityPub, you should enable media pages so that media can be properly accessed and interacted with by ActivityPub clients and other federated platforms.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\sprintf(
|
||||
// translators: %s: Yoast SEO settings URL.
|
||||
\__( 'You can enable media pages in <a href="%s">Yoast SEO > Settings > Advanced > Media pages</a>.', 'activitypub' ),
|
||||
\esc_url( \admin_url( 'admin.php?page=wpseo_page_settings#/media-pages' ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Yoast SEO media pages are disabled.
|
||||
*
|
||||
* @return bool True if media pages are disabled, false otherwise.
|
||||
*/
|
||||
public static function is_media_pages_disabled() {
|
||||
// Get Yoast SEO options.
|
||||
$yoast_options = \get_option( 'wpseo_titles' );
|
||||
|
||||
if ( ! is_array( $yoast_options ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if disable-attachment is set to true (media pages disabled).
|
||||
return isset( $yoast_options['disable-attachment'] ) && true === $yoast_options['disable-attachment'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if attachment post type is supported by ActivityPub.
|
||||
*
|
||||
* @return bool True if attachment is supported, false otherwise.
|
||||
*/
|
||||
private static function is_attachment_supported() {
|
||||
$supported_post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
return in_array( 'attachment', $supported_post_types, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if Yoast's "Enable author archives" setting is properly configured.
|
||||
*
|
||||
* @return array The test result.
|
||||
*/
|
||||
public static function test_yoast_seo_author_archives() {
|
||||
$result = array(
|
||||
'label' => \__( 'Yoast SEO author archives are enabled', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Author archives are enabled in Yoast SEO, which allows author archives to be federated and interacted with through ActivityPub.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_yoast_seo_author_archives',
|
||||
);
|
||||
|
||||
if ( self::is_author_archives_disabled() ) {
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Yoast SEO author archives are not enabled', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'The “Enable author archives” setting in Yoast SEO is currently disabled. Author archives are essential for ActivityPub, as they act as user profile pages. Without them, other platforms won’t be able to view those profiles.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\sprintf(
|
||||
// translators: %s: Yoast SEO settings URL.
|
||||
\__( 'You can enable author archives in <a href="%s">Yoast SEO > Settings > Advanced > Author archives</a>.', 'activitypub' ),
|
||||
\esc_url( \admin_url( 'admin.php?page=wpseo_page_settings#/author-archives' ) )
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Yoast SEO author archives are disabled.
|
||||
*
|
||||
* @return bool True if author archives are disabled, false otherwise.
|
||||
*/
|
||||
public static function is_author_archives_disabled() {
|
||||
// Get Yoast SEO options.
|
||||
$yoast_options = \get_option( 'wpseo_titles' );
|
||||
|
||||
if ( ! is_array( $yoast_options ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if disable-author is set (author archives disabled).
|
||||
return (bool) ( $yoast_options['disable-author'] ?? false );
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
/**
|
||||
* Load the ActivityPub integrations.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\site_supports_blocks;
|
||||
|
||||
\Activitypub\Autoloader::register_path( __NAMESPACE__, __DIR__ );
|
||||
|
||||
/**
|
||||
* Initialize the ActivityPub integrations.
|
||||
*/
|
||||
function plugin_init() {
|
||||
/**
|
||||
* Adds Akismet support.
|
||||
*
|
||||
* This class handles the compatibility with the Akismet plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/akismet/
|
||||
*/
|
||||
if ( \defined( 'AKISMET_VERSION' ) ) {
|
||||
Akismet::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Classic Editor support.
|
||||
*
|
||||
* This class handles the compatibility with the Classic Editor plugin
|
||||
* and sites without block editor support.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/classic-editor/
|
||||
*/
|
||||
if ( \class_exists( '\Classic_Editor' ) || \function_exists( 'classicpress_version' ) || ! site_supports_blocks() ) {
|
||||
Classic_Editor::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Enable Mastodon Apps support.
|
||||
*
|
||||
* This class handles the compatibility with the Enable Mastodon Apps plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/enable-mastodon-apps/
|
||||
*/
|
||||
if ( \defined( 'ENABLE_MASTODON_APPS_VERSION' ) ) {
|
||||
Enable_Mastodon_Apps::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Jetpack support.
|
||||
*
|
||||
* This class handles the compatibility with Jetpack.
|
||||
*
|
||||
* @see https://jetpack.com/
|
||||
*/
|
||||
if ( \defined( 'JETPACK__VERSION' ) ) {
|
||||
Jetpack::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds LiteSpeed Cache support.
|
||||
*
|
||||
* The check for whether LiteSpeed Cache is loaded and initialized happens inside Litespeed_Cache::init().
|
||||
*
|
||||
* @see https://wordpress.org/plugins/litespeed-cache/
|
||||
*/
|
||||
Litespeed_Cache::init();
|
||||
|
||||
/**
|
||||
* Adds Multisite Language Switcher support.
|
||||
*
|
||||
* This class handles the compatibility with the Multisite Language Switcher plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/multisite-language-switcher/
|
||||
*/
|
||||
if ( \defined( 'MSLS_PLUGIN_VERSION' ) ) {
|
||||
Multisite_Language_Switcher::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds NodeInfo (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the NodeInfo plugin
|
||||
* and coordinates the internal NodeInfo implementation.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/nodeinfo/
|
||||
*/
|
||||
Nodeinfo::init();
|
||||
|
||||
/**
|
||||
* Adds OpenGraph support.
|
||||
*
|
||||
* This class handles the compatibility with the OpenGraph plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/opengraph/
|
||||
*/
|
||||
if ( '1' === \get_option( 'activitypub_use_opengraph', '1' ) ) {
|
||||
Opengraph::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Podlove Podcast Publisher support.
|
||||
*
|
||||
* This class handles the compatibility with Podlove Podcast Publisher.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/podlove-podcasting-plugin-for-wordpress/
|
||||
*/
|
||||
if ( \defined( 'Podlove\PLUGIN_FILE' ) ) {
|
||||
// Enable ActivityPub support for the podcast post type.
|
||||
\add_post_type_support( 'podcast', 'activitypub' );
|
||||
|
||||
\add_filter(
|
||||
'activitypub_transformer',
|
||||
static function ( $transformer, $data, $object_class ) {
|
||||
if (
|
||||
'WP_Post' === $object_class &&
|
||||
'podcast' === $data->post_type &&
|
||||
\Podlove\Model\Episode::find_one_by_post_id( $data->ID )
|
||||
) {
|
||||
return new Podlove_Podcast_Publisher( $data );
|
||||
}
|
||||
return $transformer;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Seriously Simple Podcasting support.
|
||||
*
|
||||
* This class handles the compatibility with Seriously Simple Podcasting.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
|
||||
*/
|
||||
if ( \defined( 'SSP_VERSION' ) ) {
|
||||
add_filter(
|
||||
'activitypub_transformer',
|
||||
static function ( $transformer, $data, $object_class ) {
|
||||
if (
|
||||
'WP_Post' === $object_class &&
|
||||
\get_post_meta( $data->ID, 'audio_file', true )
|
||||
) {
|
||||
return new Seriously_Simple_Podcasting( $data );
|
||||
}
|
||||
return $transformer;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Stream support.
|
||||
*
|
||||
* This class handles the compatibility with the Stream plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/stream/
|
||||
*/
|
||||
Stream\Stream::init();
|
||||
|
||||
/**
|
||||
* Adds Surge support.
|
||||
*
|
||||
* Only load code that needs Surge to run once Surge is loaded and initialized.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/surge/
|
||||
*/
|
||||
Surge::init();
|
||||
|
||||
/**
|
||||
* Adds WebFinger (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the WebFinger plugin
|
||||
* and coordinates the internal WebFinger implementation.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/webfinger/
|
||||
*/
|
||||
Webfinger::init();
|
||||
|
||||
/**
|
||||
* Adds WP REST Cache support.
|
||||
*
|
||||
* This class handles the compatibility with the WP REST Cache plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/wp-rest-cache/
|
||||
*/
|
||||
if ( \class_exists( 'WP_Rest_Cache_Plugin\Includes\Plugin' ) ) {
|
||||
WP_Rest_Cache::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds WPML Multilingual CMS (plugin) support.
|
||||
*
|
||||
* This class handles the compatibility with the WPML plugin.
|
||||
*
|
||||
* @see https://wpml.org/
|
||||
*/
|
||||
if ( \defined( 'ICL_SITEPRESS_VERSION' ) ) {
|
||||
WPML::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Yoast SEO support.
|
||||
*
|
||||
* This class handles the compatibility with Yoast SEO.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/wordpress-seo/
|
||||
*/
|
||||
if ( \defined( 'WPSEO_VERSION' ) ) {
|
||||
Yoast_Seo::init();
|
||||
}
|
||||
}
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
|
||||
|
||||
// Register activation and deactivation hooks for Surge integration.
|
||||
\register_activation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Surge', 'add_cache_config' ) );
|
||||
\register_deactivation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\Surge', 'remove_cache_config' ) );
|
||||
|
||||
// Register activation and deactivation hooks for LiteSpeed Cache integration.
|
||||
\register_activation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\LiteSpeed_Cache', 'add_htaccess_rules' ) );
|
||||
\register_deactivation_hook( ACTIVITYPUB_PLUGIN_FILE, array( __NAMESPACE__ . '\LiteSpeed_Cache', 'remove_htaccess_rules' ) );
|
||||
|
||||
/**
|
||||
* Load the BuddyPress integration.
|
||||
*
|
||||
* Only load code that needs BuddyPress to run once BP is loaded and initialized.
|
||||
*
|
||||
* @see https://buddypress.org/
|
||||
*/
|
||||
\add_action( 'bp_include', array( __NAMESPACE__ . '\Buddypress', 'init' ), 0 );
|
||||
@ -0,0 +1,254 @@
|
||||
<?php
|
||||
/**
|
||||
* Stream Connector integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration\Stream;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\url_to_commentid;
|
||||
|
||||
/**
|
||||
* Stream Connector for ActivityPub.
|
||||
*
|
||||
* This class is a Stream Connector for the Stream plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/stream/
|
||||
*/
|
||||
class Connector extends \WP_Stream\Connector {
|
||||
/**
|
||||
* Connector slug.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = 'activitypub';
|
||||
|
||||
/**
|
||||
* Actions registered for this connector.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $actions = array(
|
||||
'activitypub_handled_follow',
|
||||
'activitypub_sent_to_inbox',
|
||||
'activitypub_outbox_processing_complete',
|
||||
'activitypub_outbox_processing_batch_complete',
|
||||
);
|
||||
|
||||
/**
|
||||
* Return translated connector label.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_label() {
|
||||
return \__( 'ActivityPub', 'activitypub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return translated context labels.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_context_labels() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return translated action labels.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_action_labels() {
|
||||
return array(
|
||||
'processed' => \__( 'Processed', 'activitypub' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add action links to Stream drop row in admin list screen
|
||||
*
|
||||
* @filter wp_stream_action_links_{connector}
|
||||
*
|
||||
* @param array $links Previous links registered.
|
||||
* @param Record $record Stream record.
|
||||
*
|
||||
* @return array Action links
|
||||
*/
|
||||
public function action_links( $links, $record ) {
|
||||
if ( 'processed' === $record->action ) {
|
||||
$error = \json_decode( $record->get_meta( 'error', true ), true );
|
||||
|
||||
if ( $error ) {
|
||||
$message = \sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
\__( 'Inbox Error', 'activitypub' ),
|
||||
\wp_json_encode( $error )
|
||||
);
|
||||
|
||||
$links[ $message ] = '';
|
||||
}
|
||||
|
||||
$debug = \json_decode( $record->get_meta( 'debug', true ), true );
|
||||
|
||||
if ( $debug ) {
|
||||
$message = \sprintf(
|
||||
'<details><summary>%1$s</summary><pre>%2$s</pre></details>',
|
||||
\__( 'Debug', 'activitypub' ),
|
||||
\wp_json_encode( $debug )
|
||||
);
|
||||
|
||||
$links[ $message ] = '';
|
||||
}
|
||||
}
|
||||
|
||||
return $links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_handled_follow.
|
||||
*
|
||||
* @param array $activity The ActivityPub activity data.
|
||||
* @param int|null $user_id The local user ID, or null if not applicable.
|
||||
* @param mixed $state Status or WP_Error object indicating the result of the follow handling.
|
||||
* @param \WP_Post|null $context The WP_Post object representing the remote actor/follower.
|
||||
*/
|
||||
public function callback_activitypub_handled_follow( $activity, $user_id, $state, $context ) {
|
||||
$actor_url = \is_object( $context ) && ! \is_wp_error( $context ) ? $context->guid : $activity['actor'];
|
||||
|
||||
$this->log(
|
||||
\sprintf(
|
||||
// translators: %s is a URL.
|
||||
\__( 'New Follower: %s', 'activitypub' ),
|
||||
$actor_url
|
||||
),
|
||||
array(
|
||||
'activity' => \wp_json_encode( $activity, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
|
||||
'remote_actor' => \wp_json_encode( $context, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
|
||||
),
|
||||
null,
|
||||
'notification',
|
||||
'follow',
|
||||
$user_id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_outbox_processing_complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
public function callback_activitypub_outbox_processing_complete( $inboxes, $json, $actor_id, $outbox_item_id ) {
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
$outbox_data = $this->prepare_outbox_data_for_response( $outbox_item );
|
||||
|
||||
$this->log(
|
||||
\sprintf(
|
||||
// translators: %s is a URL.
|
||||
\__( 'Outbox processing complete: %s', 'activitypub' ),
|
||||
$outbox_data['title']
|
||||
),
|
||||
array(
|
||||
'debug' => \wp_json_encode(
|
||||
array(
|
||||
'actor_id' => $actor_id,
|
||||
'outbox_item_id' => $outbox_item_id,
|
||||
)
|
||||
),
|
||||
),
|
||||
$outbox_data['id'],
|
||||
$outbox_data['type'],
|
||||
'processed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for activitypub_outbox_processing_batch_complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
public function callback_activitypub_outbox_processing_batch_complete( $inboxes, $json, $actor_id, $outbox_item_id, $batch_size, $offset ) {
|
||||
$outbox_item = \get_post( $outbox_item_id );
|
||||
$outbox_data = $this->prepare_outbox_data_for_response( $outbox_item );
|
||||
|
||||
$this->log(
|
||||
// translators: %s is a URL.
|
||||
\sprintf( \__( 'Outbox processing batch complete: %s', 'activitypub' ), $outbox_data['title'] ),
|
||||
array(
|
||||
'debug' => \wp_json_encode(
|
||||
array(
|
||||
'actor_id' => $actor_id,
|
||||
'outbox_item_id' => $outbox_item_id,
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset,
|
||||
)
|
||||
),
|
||||
),
|
||||
$outbox_data['id'],
|
||||
$outbox_data['type'],
|
||||
'processed'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of the outbox object.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The outbox item.
|
||||
*
|
||||
* @return array The title, object ID, and object type of the outbox object.
|
||||
*/
|
||||
protected function prepare_outbox_data_for_response( $outbox_item ) {
|
||||
$object_id = $outbox_item->ID;
|
||||
$object_type = $outbox_item->post_type;
|
||||
$object_title = $outbox_item->post_title;
|
||||
|
||||
$post_id = \url_to_postid( $outbox_item->post_title );
|
||||
if ( $post_id ) {
|
||||
$post = \get_post( $post_id );
|
||||
|
||||
$object_id = $post_id;
|
||||
$object_type = $post->post_type;
|
||||
$object_title = $post->post_title;
|
||||
} else {
|
||||
$comment_id = url_to_commentid( $outbox_item->post_title );
|
||||
if ( $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
$object_id = $comment_id;
|
||||
$object_type = 'comments';
|
||||
$object_title = $comment->comment_content;
|
||||
} else {
|
||||
$author_id = url_to_authorid( $outbox_item->post_title );
|
||||
if ( null !== $author_id ) {
|
||||
$object_id = $author_id;
|
||||
$object_type = 'profiles';
|
||||
|
||||
if ( $author_id ) {
|
||||
$object_title = \get_userdata( $author_id )->display_name;
|
||||
} elseif ( Actors::BLOG_USER_ID === $author_id ) {
|
||||
$object_title = \__( 'Blog User', 'activitypub' );
|
||||
} elseif ( Actors::APPLICATION_USER_ID === $author_id ) {
|
||||
$object_title = \__( 'Application User', 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'id' => $object_id,
|
||||
'type' => $object_type,
|
||||
'title' => $object_title,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Stream integration file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Integration\Stream;
|
||||
|
||||
/**
|
||||
* Stream integration.
|
||||
*
|
||||
* This class handles the compatibility with the Stream plugin.
|
||||
*
|
||||
* @see https://wordpress.org/plugins/stream/
|
||||
*/
|
||||
class Stream {
|
||||
/**
|
||||
* Initialize the Stream integration.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'wp_stream_connectors', array( self::class, 'register_connector' ) );
|
||||
\add_filter( 'wp_stream_posts_exclude_post_types', array( self::class, 'exclude_post_types' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Stream Connector for ActivityPub.
|
||||
*
|
||||
* @param array $classes The Stream connectors.
|
||||
*
|
||||
* @return array The Stream connectors with the ActivityPub connector.
|
||||
*/
|
||||
public static function register_connector( $classes ) {
|
||||
$class = new Connector();
|
||||
|
||||
if ( \method_exists( $class, 'is_dependency_satisfied' ) && $class->is_dependency_satisfied() ) {
|
||||
$classes[] = $class;
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude ActivityPub post types from the Stream.
|
||||
*
|
||||
* @param array $post_types The post types to exclude.
|
||||
*
|
||||
* @return array The post types to exclude with ActivityPub post types.
|
||||
*/
|
||||
public static function exclude_post_types( $post_types ) {
|
||||
$post_types[] = 'ap_actor';
|
||||
$post_types[] = 'ap_extrafield';
|
||||
$post_types[] = 'ap_extrafield_blog';
|
||||
$post_types[] = 'ap_post';
|
||||
|
||||
return $post_types;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* Content negotiation fix for Surge.
|
||||
*
|
||||
* @see https://dominikschilling.de/notes/http-accept-header-wordpress-cache-activitypub/
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
$representation = 'html';
|
||||
|
||||
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
$accept = strtolower( $_SERVER['HTTP_ACCEPT'] );
|
||||
|
||||
if ( str_contains( $accept, 'text/html' ) ) {
|
||||
$representation = 'html';
|
||||
} elseif (
|
||||
str_contains( $accept, 'application/json' ) ||
|
||||
str_contains( $accept, 'application/activity+json' ) ||
|
||||
str_contains( $accept, 'application/ld+json' )
|
||||
) {
|
||||
$representation = 'json';
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
|
||||
$config['variants']['representation'] = $representation;
|
||||
unset( $accept, $representation );
|
||||
|
||||
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
|
||||
return $config;
|
||||
Reference in New Issue
Block a user