Skip to content

Commit

Permalink
Merge branch 'trunk' into improve/preview-row-action
Browse files Browse the repository at this point in the history
  • Loading branch information
pfefferle authored Nov 19, 2024
2 parents 0ccb617 + 6ec77cc commit c72d00c
Show file tree
Hide file tree
Showing 39 changed files with 2,158 additions and 856 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Dev
## 4.2.0 - 2024-11-15

### Added

Expand Down Expand Up @@ -1063,6 +1063,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* initial

[4.2.0]: https://github.com/Automattic/wordpress-activitypub/compare/4.1.1...4.2.0
[4.1.1]: https://github.com/Automattic/wordpress-activitypub/compare/4.1.0...4.1.1
[4.1.0]: https://github.com/Automattic/wordpress-activitypub/compare/4.0.2...4.1.0
[4.0.2]: https://github.com/Automattic/wordpress-activitypub/compare/4.0.1...4.0.2
Expand Down
2 changes: 1 addition & 1 deletion bin/install-wp-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ install_test_suite() {
svn co --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
fi

if [ ! -f wp-tests-config.php ]; then
if [ ! -f "$WP_TESTS_DIR"/wp-tests-config.php ]; then
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
# remove all forward slashes in the end
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
Expand Down
16 changes: 16 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -1471,3 +1471,19 @@ function get_attribution_domains() {

return $domains;
}

/**
* Get the base URL for uploads.
*
* @return string The upload base URL.
*/
function get_upload_baseurl() {
$upload_dir = \wp_get_upload_dir();

/**
* Filters the upload base URL.
*
* @param string \wp_get_upload_dir()['baseurl'] The upload base URL.
*/
return apply_filters( 'activitypub_get_upload_baseurl', $upload_dir['baseurl'] );
}
23 changes: 19 additions & 4 deletions includes/transformer/class-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_enclosures;
use function Activitypub\get_upload_baseurl;
use function Activitypub\get_content_warning;
use function Activitypub\site_supports_blocks;
use function Activitypub\generate_post_summary;
Expand Down Expand Up @@ -477,14 +478,22 @@ protected function get_classic_editor_image_embeds( $max_images ) {
}

$images = array();
$base = \wp_get_upload_dir()['baseurl'];
$base = get_upload_baseurl();
$content = \get_post_field( 'post_content', $this->wp_object );
$tags = new \WP_HTML_Tag_Processor( $content );

// This linter warning is a false positive - we have to re-count each time here as we modify $images.
// phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) {
$src = $tags->get_attribute( 'src' );
/**
* Filter the image source URL.
*
* This can be used to modify the image source URL before it is used to
* determine the attachment ID.
*
* @param string $src The image source URL.
*/
$src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) );

/*
* If the img source is in our uploads dir, get the
Expand All @@ -499,16 +508,22 @@ protected function get_classic_editor_image_embeds( $max_images ) {
if ( null !== $src && \str_starts_with( $src, $base ) ) {
$img_id = \attachment_url_to_postid( $src );

if ( 0 === $img_id ) {
$count = 0;
$src = \strtok( $src, '?' );
$img_id = \attachment_url_to_postid( $src );
}

if ( 0 === $img_id ) {
$count = 0;
$src = preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
$src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count );
if ( $count > 0 ) {
$img_id = \attachment_url_to_postid( $src );
}
}

if ( 0 === $img_id ) {
$src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
$src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
$img_id = \attachment_url_to_postid( $src );
}

Expand Down
157 changes: 154 additions & 3 deletions integration/class-enable-mastodon-apps.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Activitypub\Collection\Actors;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Extra_Fields;
use Activitypub\Transformer\Factory;
use Enable_Mastodon_Apps\Mastodon_API;
use Enable_Mastodon_Apps\Entity\Account;
use Enable_Mastodon_Apps\Entity\Status;
Expand All @@ -36,6 +37,7 @@ public static function init() {
\add_filter( 'mastodon_api_account_followers', array( self::class, 'api_account_followers' ), 10, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 15, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_internal' ), 9, 2 );
\add_filter( 'mastodon_api_status', array( self::class, 'api_status' ), 9, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search' ), 40, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search_by_url' ), 40, 2 );
\add_filter( 'mastodon_api_get_posts_query_args', array( self::class, 'api_get_posts_query_args' ) );
Expand Down Expand Up @@ -324,6 +326,150 @@ function ( $field ) {
return $account;
}

/**
* Use our representation of posts to power each status item.
* Includes proper referncing of 3rd party comments that arrived via federation.
*
* @param null|Status $status The status, typically null to allow later filters their shot.
* @param int $post_id The post ID.
* @return Status|null The status.
*/
public static function api_status( $status, $post_id ) {
$post = \get_post( $post_id );
if ( ! $post ) {
return $status;
}

// EMA makes a `comment` post_type to mirror comments and so that there can be a single get_posts() call for everything.
if ( get_post_type( $post ) === 'comment' ) {
$comment_id = get_post_meta( $post->ID, 'comment_id', true );
if ( $comment_id ) {
return self::api_comment_status( $comment_id, $post_id );
}
}

return self::api_post_status( $post_id );
}

/**
* Transforms a WordPress post into a Mastodon-compatible status object.
*
* Takes a post ID, transforms it into an ActivityPub object, and converts
* it to a Mastodon API status format including the author's account info.
*
* @param int $post_id The WordPress post ID to transform.
* @return Status|null The Mastodon API status object, or null if the post is not found
*/
private static function api_post_status( $post_id ) {
$post = Factory::get_transformer( get_post( $post_id ) );
$data = $post->to_object()->to_array();
$account = self::api_account_internal( null, get_post_field( 'post_author', $post_id ) );
return self::activity_to_status( $data, $account, $post_id );
}

/**
* Traditional WP commenters may leave a URL, which itself may be a valid actor.
* If so, we'll use that actor's data to represent the comment.
*
* @param string $url The URL.
* @return Account|false The account or false.
*/
private static function maybe_get_account_for_actor( $url ) {
if ( empty( $url ) ) {
return false;
}
$uri = Webfinger_Util::resolve( $url );
if ( $uri && ! is_wp_error( $uri ) ) {
return self::get_account_for_actor( $uri );
}
// Next, if the URL does not have a path, we'll try to resolve it in the form of [email protected].
$parts = \wp_parse_url( $url );
if ( ( ! isset( $parts['path'] ) || ! $parts['path'] ) && isset( $parts['host'] ) ) {
$url = trailingslashit( $url ) . '@' . $parts['host'];
$acct = Webfinger_Util::uri_to_acct( $url );
if ( $acct && ! is_wp_error( $acct ) ) {
return self::get_account_for_actor( $acct );
}
}

return false;
}

/**
* Convert an local WP comment into a pseudo-account, after first checking if their
* supplied URL is a valid actor.
*
* @param \WP_Comment $comment The comment.
* @return Account The account.
*/
private static function get_account_for_local_comment( $comment ) {
$maybe_actor = self::maybe_get_account_for_actor( $comment->comment_author_url );
if ( $maybe_actor ) {
return $maybe_actor;
}

// We will make a pretend local account for this comment.
$account = new Account();
$account->id = 999999; // This is a fake ID.
$account->username = $comment->comment_author;
$account->acct = sprintf( 'comments@%s', wp_parse_url( home_url(), PHP_URL_HOST ) );
$account->display_name = $comment->comment_author;
$account->url = get_comment_link( $comment );
$account->avatar = get_avatar_url( $comment->comment_author_email );
$account->avatar_static = $account->avatar;
$account->created_at = new DateTime( $comment->comment_date_gmt );
$account->last_status_at = new DateTime( $comment->comment_date_gmt );
$account->note = sprintf(
/* translators: %s: comment author name */
__( 'This is a local comment by %s, not a fediverse comment. This profile cannot be followed.', 'activitypub' ),
$comment->comment_author
);

return $account;
}

/**
* Convert a WordPress comment to a Status.
*
* @param int $comment_id The comment ID.
* @param int $post_id The post ID (this is the mirrored `comment` post).
*
* @return Status|null The status.
*/
private static function api_comment_status( $comment_id, $post_id ) {
$comment = get_comment( $comment_id );
$post = get_post( $post_id );
if ( ! $comment || ! $post ) {
return null;
}

$is_remote_comment = get_comment_meta( $comment->comment_ID, 'protocol', true ) === 'activitypub';

if ( $is_remote_comment ) {
$account = self::get_account_for_actor( $comment->comment_author_url );
// @todo fallback to locally stored data from the time the comment was made,
// if the remote actor is not found/no longer available.
} else {
$account = self::get_account_for_local_comment( $comment );
}

if ( ! $account ) {
return null;
}

$status = new Status();
$status->id = $comment->comment_ID;
$status->created_at = new DateTime( $comment->comment_date_gmt );
$status->content = $comment->comment_content;
$status->account = $account;
$status->visibility = 'public';
$status->uri = get_comment_link( $comment );
$status->in_reply_to_id = $post->post_parent;

return $status;
}


/**
* Get account for actor.
*
Expand All @@ -332,7 +478,7 @@ function ( $field ) {
* @return Account|null The account.
*/
private static function get_account_for_actor( $uri ) {
if ( ! is_string( $uri ) ) {
if ( ! is_string( $uri ) || empty( $uri ) ) {
return null;
}
$data = get_remote_metadata_by_actor( $uri );
Expand All @@ -343,6 +489,10 @@ private static function get_account_for_actor( $uri ) {
$account = new Account();

$acct = Webfinger_Util::uri_to_acct( $uri );
if ( ! $acct || is_wp_error( $acct ) ) {
return null;
}

if ( str_starts_with( $acct, 'acct:' ) ) {
$acct = substr( $acct, 5 );
}
Expand Down Expand Up @@ -489,10 +639,11 @@ public static function api_get_posts_query_args( $args ) {
*
* @param array $item The activity.
* @param Account $account The account.
* @param int $post_id The post ID. Optional, but will be preferred in the Status.
*
* @return Status|null The status.
*/
private static function activity_to_status( $item, $account ) {
private static function activity_to_status( $item, $account, $post_id = null ) {
if ( isset( $item['object'] ) ) {
$object = $item['object'];
} else {
Expand All @@ -504,7 +655,7 @@ private static function activity_to_status( $item, $account ) {
}

$status = new Status();
$status->id = $object['id'];
$status->id = $post_id ?? $object['id'];
$status->created_at = new DateTime( $object['published'] );
$status->content = $object['content'];
$status->account = $account;
Expand Down
1 change: 0 additions & 1 deletion phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<exclude-pattern>*\.(inc|css|js|svg)</exclude-pattern>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/tests/*</exclude-pattern>
<exclude-pattern>*.asset.php</exclude-pattern>

<arg value="ps"/>
Expand Down
1 change: 0 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
>
<testsuites>
<testsuite name="ActivityPub">
<directory prefix="test-" suffix=".php">./tests/</directory>
<directory prefix="class-test-" suffix=".php">./tests/</directory>
</testsuite>
</testsuites>
Expand Down
Binary file added tests/assets/test.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
<?php
/**
* Bootstrap file for ActivityPub.
*
* @package Activitypub
*/

\define( 'ACTIVITYPUB_DISABLE_REACTIONS', false );
\define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );

Expand Down
Loading

0 comments on commit c72d00c

Please sign in to comment.