updated plugin ActivityPub version 9.1.0
This commit is contained in:
@ -8,7 +8,6 @@
|
||||
namespace Activitypub\Transformer;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Blocks;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Interactions;
|
||||
use Activitypub\Collection\Replies;
|
||||
@ -21,6 +20,7 @@ use function Activitypub\get_content_visibility;
|
||||
use function Activitypub\get_content_warning;
|
||||
use function Activitypub\get_enclosures;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\is_post_publicly_queryable;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\site_supports_blocks;
|
||||
|
||||
@ -90,6 +90,28 @@ class Post extends Base {
|
||||
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
||||
*/
|
||||
public function to_object() {
|
||||
/*
|
||||
* A redacted (password-protected or non-public) post is, from the
|
||||
* Fediverse's perspective, gone — the soft-delete path that reaches here
|
||||
* emits a Delete. Represent it as a Tombstone: content-free by type, so
|
||||
* no body-derived field (content, summary, tags, @-mentions, location,
|
||||
* attachments…) can ever leak, even one added to the transformer later.
|
||||
*
|
||||
* Address the teardown to the public collection. A post only reaches the
|
||||
* soft-delete path after being federated, and only public / quiet-public
|
||||
* posts federate (private and local ones never do), so the original
|
||||
* audience was always public — broadcasting the Delete tears the copy
|
||||
* down everywhere it may exist. Private/direct activities are deleted via
|
||||
* their own outbox path and keep their original (non-public) audience, so
|
||||
* they are not affected by this.
|
||||
*/
|
||||
if ( $this->is_redacted() ) {
|
||||
$tombstone = $this->to_tombstone();
|
||||
$tombstone->set_to( array( 'https://www.w3.org/ns/activitystreams#Public' ) );
|
||||
|
||||
return $tombstone;
|
||||
}
|
||||
|
||||
$post = $this->item;
|
||||
$object = parent::to_object();
|
||||
|
||||
@ -113,6 +135,9 @@ class Post extends Base {
|
||||
$object = new Base_Object();
|
||||
$object->set_type( 'Tombstone' );
|
||||
$object->set_id( $this->get_id() );
|
||||
// Preserve the permalink so the tombstone registry can resolve a request
|
||||
// to it, even on sites whose ActivityPub ID is the post-ID URL (?p=123).
|
||||
$object->set_url( $this->get_url() );
|
||||
$object->set_former_type( $this->get_type() );
|
||||
$object->set_published( $this->get_published() );
|
||||
$object->set_updated( $this->get_updated() );
|
||||
@ -175,7 +200,7 @@ class Post extends Base {
|
||||
|
||||
$user = Actors::get_by_id( $this->item->post_author );
|
||||
|
||||
if ( $user && ! is_wp_error( $user ) ) {
|
||||
if ( $user && ! \is_wp_error( $user ) ) {
|
||||
$this->actor_object = $user;
|
||||
return $user;
|
||||
}
|
||||
@ -186,6 +211,20 @@ class Post extends Base {
|
||||
/**
|
||||
* Returns the ID of the Post.
|
||||
*
|
||||
* Posts past `activitypub_last_post_with_permalink_as_id` use the post-ID URL
|
||||
* as their canonical ActivityPub ID — stable across slug changes. Posts at
|
||||
* or below the threshold are *legacy* and use their permalink as the ID,
|
||||
* which means a slug change effectively renames the federated object.
|
||||
*
|
||||
* Known limitation: a legacy post whose slug changes in the same save as
|
||||
* a soft-delete transition (e.g. publish → draft + new post_name) will
|
||||
* emit a Delete targeting the new permalink, while remote servers cached
|
||||
* the original. The trash case mitigates this via the `wp_trash_post`
|
||||
* hook caching the pre-transition URL in `_activitypub_canonical_url`,
|
||||
* but draft / pending / private / password-applied transitions do not.
|
||||
* If you maintain a site that pre-dates the ID migration, avoid editing
|
||||
* the slug in the same save as the visibility change.
|
||||
*
|
||||
* @return string The Posts ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
@ -225,7 +264,7 @@ class Post extends Base {
|
||||
break;
|
||||
}
|
||||
|
||||
return \esc_url( $permalink );
|
||||
return \esc_url_raw( $permalink );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -265,7 +304,7 @@ class Post extends Base {
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*/
|
||||
$thumbnail = apply_filters(
|
||||
$thumbnail = \apply_filters(
|
||||
'activitypub_get_image',
|
||||
$this->get_attachment_image_src( $id, $image_size ),
|
||||
$id,
|
||||
@ -280,7 +319,7 @@ class Post extends Base {
|
||||
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => \esc_url( $thumbnail[0] ),
|
||||
'url' => \esc_url_raw( $thumbnail[0] ),
|
||||
'mediaType' => \esc_attr( $mime_type ),
|
||||
);
|
||||
|
||||
@ -305,7 +344,7 @@ class Post extends Base {
|
||||
$id = \get_post_thumbnail_id( $post_id );
|
||||
} else {
|
||||
// Try site_logo, falling back to site_icon, first.
|
||||
$id = get_option( 'site_icon' );
|
||||
$id = \get_option( 'site_icon' );
|
||||
}
|
||||
|
||||
if ( ! $id ) {
|
||||
@ -321,7 +360,7 @@ class Post extends Base {
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*/
|
||||
$thumbnail = apply_filters(
|
||||
$thumbnail = \apply_filters(
|
||||
'activitypub_get_image',
|
||||
$this->get_attachment_image_src( $id, $image_size ),
|
||||
$id,
|
||||
@ -336,7 +375,7 @@ class Post extends Base {
|
||||
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => \esc_url( $thumbnail[0] ),
|
||||
'url' => \esc_url_raw( $thumbnail[0] ),
|
||||
'mediaType' => \esc_attr( $mime_type ),
|
||||
);
|
||||
|
||||
@ -358,19 +397,9 @@ class Post extends Base {
|
||||
return $this->attachment;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove attachments from the Fediverse if a post was federated and then unpublished.
|
||||
* Except in preview mode, where we want to show attachments.
|
||||
*/
|
||||
if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
|
||||
$this->attachment = array();
|
||||
|
||||
return $this->attachment;
|
||||
}
|
||||
|
||||
$max_media = \get_post_meta( $this->item->ID, 'activitypub_max_image_attachments', true );
|
||||
|
||||
if ( ! is_numeric( $max_media ) ) {
|
||||
if ( ! \is_numeric( $max_media ) ) {
|
||||
$max_media = \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS );
|
||||
}
|
||||
|
||||
@ -523,7 +552,7 @@ class Post extends Base {
|
||||
|
||||
$tags[] = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
|
||||
'href' => \esc_url_raw( \get_tag_link( $post_tag->term_id ) ),
|
||||
'name' => esc_hashtag( $post_tag->name ),
|
||||
);
|
||||
}
|
||||
@ -551,13 +580,6 @@ class Post extends Base {
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
// Remove Teaser from unpublished posts.
|
||||
if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
|
||||
$this->summary = \__( '(This post is being modified)', 'activitypub' );
|
||||
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
$this->summary = generate_post_summary( $this->item );
|
||||
|
||||
return $this->summary;
|
||||
@ -601,13 +623,6 @@ class Post extends Base {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
// Remove Content from unpublished posts.
|
||||
if ( ! $this->is_preview() && 'publish' !== \get_post_status( $this->item ) ) {
|
||||
$this->content = \__( '(This post is being modified)', 'activitypub' );
|
||||
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
global $post;
|
||||
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
@ -624,7 +639,7 @@ class Post extends Base {
|
||||
\do_action( 'activitypub_before_get_content', $post );
|
||||
|
||||
// It seems that shortcodes are only applied to published posts.
|
||||
if ( is_preview() ) {
|
||||
if ( \is_preview() ) {
|
||||
$post->post_status = 'publish';
|
||||
}
|
||||
|
||||
@ -649,22 +664,6 @@ class Post extends Base {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML @ link for reply block.
|
||||
*
|
||||
* @deprecated 7.4.0 Use {@see Blocks::generate_reply_link()}.
|
||||
*
|
||||
* @param string $block_content The block content.
|
||||
* @param array $block The block data.
|
||||
*
|
||||
* @return string The HTML @ link.
|
||||
*/
|
||||
public function generate_reply_link( $block_content, $block ) {
|
||||
_deprecated_function( __METHOD__, '7.4.0', 'Activitypub\Blocks::generate_reply_link' );
|
||||
|
||||
return Blocks::generate_reply_link( $block_content, $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-reply-to URL of the post.
|
||||
*
|
||||
@ -701,7 +700,7 @@ class Post extends Base {
|
||||
return $this->in_reply_to;
|
||||
}
|
||||
|
||||
if ( 1 === count( $reply_urls ) ) {
|
||||
if ( 1 === \count( $reply_urls ) ) {
|
||||
$this->in_reply_to = \current( $reply_urls );
|
||||
|
||||
return $this->in_reply_to;
|
||||
@ -760,8 +759,8 @@ class Post extends Base {
|
||||
|
||||
// Both latitude and longitude are required for a valid location.
|
||||
// Use is_numeric() instead of empty() since 0 is a valid coordinate (Equator/Prime Meridian).
|
||||
$has_latitude = isset( $meta['geo_latitude'][0] ) && is_numeric( $meta['geo_latitude'][0] );
|
||||
$has_longitude = isset( $meta['geo_longitude'][0] ) && is_numeric( $meta['geo_longitude'][0] );
|
||||
$has_latitude = isset( $meta['geo_latitude'][0] ) && \is_numeric( $meta['geo_latitude'][0] );
|
||||
$has_longitude = isset( $meta['geo_longitude'][0] ) && \is_numeric( $meta['geo_longitude'][0] );
|
||||
|
||||
if ( ! $has_latitude || ! $has_longitude ) {
|
||||
return null;
|
||||
@ -809,7 +808,7 @@ class Post extends Base {
|
||||
*
|
||||
* @return array The filtered mentions.
|
||||
*/
|
||||
$this->mentions = apply_filters(
|
||||
$this->mentions = \apply_filters(
|
||||
'activitypub_extract_mentions',
|
||||
array(),
|
||||
$this->item->post_content . ' ' . $this->item->post_excerpt,
|
||||
@ -820,33 +819,34 @@ class Post extends Base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Embed blocks to block level link.
|
||||
* Whether the post should be redacted from ActivityPub representations.
|
||||
*
|
||||
* Remote servers will simply drop iframe elements, rendering incomplete content.
|
||||
* Redaction is fail-closed at a single boundary: `to_object()` returns a
|
||||
* Tombstone instead of transforming the post, so no body-derived field
|
||||
* (content, summary, name, preview, attachments, image/icon, tags, mentions,
|
||||
* in-reply-to, location) is ever read — not even one added to the transformer
|
||||
* later. This is the only caller of this gate.
|
||||
*
|
||||
* @deprecated 7.4.0 Use {@see Blocks::revert_embed_links()}.
|
||||
* A post is redacted exactly when it is not publicly queryable — the same
|
||||
* predicate the scheduler uses to decide a federated post should emit a
|
||||
* Delete (`is_post_publicly_queryable()`), so the two never disagree. That
|
||||
* covers non-public status, password protection, the `local`/`private`
|
||||
* content-visibility meta, and a post type that no longer supports
|
||||
* ActivityPub. The Fediverse Preview keeps working because
|
||||
* `is_post_publicly_queryable()` itself treats a draft/pending/scheduled
|
||||
* post as queryable during a `?preview=true` request from a user who can
|
||||
* edit it.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
|
||||
* @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
|
||||
* Note: we deliberately rely on `is_post_publicly_queryable()` rather than
|
||||
* `post_password_required()`. Federation output is per-instance, never
|
||||
* per-request, and `post_password_required()` returns false when a valid
|
||||
* `wp-postpass` cookie is on the current request (e.g. an editor who unlocked
|
||||
* the post), which would leak the protected body into an outbox snapshot.
|
||||
*
|
||||
* @param string $block_content The block content (html).
|
||||
* @param object $block The block object.
|
||||
*
|
||||
* @return string A block level link
|
||||
* @return boolean True if the post must be redacted, false otherwise.
|
||||
*/
|
||||
public function revert_embed_links( $block_content, $block ) {
|
||||
_deprecated_function( __METHOD__, '7.4.0', 'Activitypub\Blocks::revert_embed_links' );
|
||||
|
||||
return Blocks::revert_embed_links( $block_content, $block );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the post is a preview.
|
||||
*
|
||||
* @return boolean True if the post is a preview, false otherwise.
|
||||
*/
|
||||
private function is_preview() {
|
||||
return defined( 'ACTIVITYPUB_PREVIEW' ) && ACTIVITYPUB_PREVIEW;
|
||||
protected function is_redacted() {
|
||||
return ! is_post_publicly_queryable( $this->item );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -954,6 +954,51 @@ class Post extends Base {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'core/media-text':
|
||||
if ( ! empty( $block['attrs']['mediaId'] ) ) {
|
||||
$media_id = $block['attrs']['mediaId'];
|
||||
|
||||
// Media & Text holds either an image or a video; the default is image.
|
||||
if ( 'video' === ( $block['attrs']['mediaType'] ?? 'image' ) ) {
|
||||
$video = array( 'id' => $media_id );
|
||||
|
||||
// The poster is stored as an HTML attribute on the <video> tag, not in block attrs.
|
||||
$processor = new \WP_HTML_Tag_Processor( $block['innerHTML'] );
|
||||
if ( $processor->next_tag( array( 'tag_name' => 'video' ) ) ) {
|
||||
$poster = $processor->get_attribute( 'poster' );
|
||||
if ( ! empty( $poster ) ) {
|
||||
$video['icon'] = \esc_url_raw( $poster );
|
||||
}
|
||||
}
|
||||
|
||||
$media['video'][] = $video;
|
||||
} else {
|
||||
$alt = '';
|
||||
$processor = new \WP_HTML_Tag_Processor( $block['innerHTML'] );
|
||||
if ( $processor->next_tag( array( 'tag_name' => 'img' ) ) ) {
|
||||
$alt = $processor->get_attribute( 'alt' ) ?? '';
|
||||
}
|
||||
|
||||
// Update alt in place if the image was already collected, so a
|
||||
// duplicate ID does not get dropped (and its alt lost) later.
|
||||
$found = false;
|
||||
foreach ( $media['image'] as $i => $image ) {
|
||||
if ( isset( $image['id'] ) && $image['id'] === $media_id ) {
|
||||
$media['image'][ $i ]['alt'] = $alt;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $found ) {
|
||||
$media['image'][] = array(
|
||||
'id' => $media_id,
|
||||
'alt' => $alt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'core/audio':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$media['audio'][] = array( 'id' => $block['attrs']['id'] );
|
||||
@ -979,9 +1024,9 @@ class Post extends Base {
|
||||
case 'jetpack/slideshow':
|
||||
case 'jetpack/tiled-gallery':
|
||||
if ( ! empty( $block['attrs']['ids'] ) ) {
|
||||
$media['image'] = array_merge(
|
||||
$media['image'] = \array_merge(
|
||||
$media['image'],
|
||||
array_map(
|
||||
\array_map(
|
||||
static function ( $id ) {
|
||||
return array( 'id' => $id );
|
||||
},
|
||||
@ -1028,22 +1073,7 @@ class Post extends Base {
|
||||
return $media[ $type ];
|
||||
}
|
||||
|
||||
return array_filter( array_merge( ...array_values( $media ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WordPress Attachment to an ActivityPub Attachment.
|
||||
*
|
||||
* @deprecated 7.2.0 Use {@see Base::transform_attachment()} instead.
|
||||
*
|
||||
* @param array $media The Attachment array.
|
||||
*
|
||||
* @return array The ActivityPub Attachment.
|
||||
*/
|
||||
public function wp_attachment_to_activity_attachment( $media ) {
|
||||
_deprecated_function( __METHOD__, '7.2.0', '\Activitypub\Transformer\Base::transform_attachment()' );
|
||||
|
||||
return parent::transform_attachment( $media );
|
||||
return \array_filter( \array_merge( ...\array_values( $media ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1054,7 +1084,7 @@ class Post extends Base {
|
||||
* @return string The context of the post.
|
||||
*/
|
||||
protected function get_context() {
|
||||
return get_rest_url_by_path( sprintf( 'posts/%d/context', $this->item->ID ) );
|
||||
return get_rest_url_by_path( \sprintf( 'posts/%d/context', $this->item->ID ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1101,7 +1131,7 @@ class Post extends Base {
|
||||
* @param \WP_Post $item The WordPress post object being transformed.
|
||||
* @param string $type ActivityStreams 2.0 Object-Type for the post.
|
||||
*/
|
||||
return apply_filters( 'activitypub_object_content_template', $template, $this->item, $type );
|
||||
return \apply_filters( 'activitypub_object_content_template', $template, $this->item, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1120,7 +1150,7 @@ class Post extends Base {
|
||||
*/
|
||||
public function get_likes() {
|
||||
return array(
|
||||
'id' => get_rest_url_by_path( sprintf( 'posts/%d/likes', $this->item->ID ) ),
|
||||
'id' => get_rest_url_by_path( \sprintf( 'posts/%d/likes', $this->item->ID ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => Interactions::count_by_type( $this->item->ID, 'like' ),
|
||||
);
|
||||
@ -1133,7 +1163,7 @@ class Post extends Base {
|
||||
*/
|
||||
public function get_shares() {
|
||||
return array(
|
||||
'id' => get_rest_url_by_path( sprintf( 'posts/%d/shares', $this->item->ID ) ),
|
||||
'id' => get_rest_url_by_path( \sprintf( 'posts/%d/shares', $this->item->ID ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => Interactions::count_by_type( $this->item->ID, 'repost' ) + Interactions::count_by_type( $this->item->ID, 'quote' ),
|
||||
);
|
||||
@ -1170,7 +1200,7 @@ class Post extends Base {
|
||||
|
||||
switch ( $policy ) {
|
||||
case ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS:
|
||||
return array( 'automaticApproval' => get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->item->post_author ) ) );
|
||||
return array( 'automaticApproval' => get_rest_url_by_path( \sprintf( 'actors/%d/followers', $this->item->post_author ) ) );
|
||||
|
||||
case ACTIVITYPUB_INTERACTION_POLICY_ME:
|
||||
return array( 'automaticApproval' => $this->get_self_interaction_policy() );
|
||||
|
||||
Reference in New Issue
Block a user