Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move: Differentiate between internal and external Move #1413

Open
wants to merge 13 commits into
base: trunk
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

* Documentation for migrating from a Mastodon instance to WordPress.
* Introduce `Inherit` as a valid activity type, to store full Activities as main object.

### Changed

Expand Down
2 changes: 1 addition & 1 deletion includes/class-activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ private static function register_post_types() {
$value = ucfirst( strtolower( $value ) );
$schema = array(
'type' => 'string',
'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
'enum' => array( 'Inherit', 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
'default' => 'Announce',
);

Expand Down
2 changes: 1 addition & 1 deletion includes/class-cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ public function move( $args ) {
$from = $args[0];
$to = $args[1];

$outbox_item_id = Move::account( $from, $to );
$outbox_item_id = Move::externally( $from, $to );

if ( is_wp_error( $outbox_item_id ) ) {
WP_CLI::error( $outbox_item_id->get_error_message() );
Expand Down
21 changes: 21 additions & 0 deletions includes/class-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class Handler {
*/
public static function init() {
self::register_handlers();

add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ), 99 );
}

/**
Expand All @@ -47,4 +49,23 @@ public static function register_handlers() {
*/
do_action( 'activitypub_register_handlers' );
}

/**
* Filter the outbox activity.
*
* @param Activity $activity The activity.
* @return Activity The activity.
*/
public static function outbox_activity( $activity ) {
if ( 'Inherit' === $activity->get_type() ) {
$inherit_activity = $activity->get_object();
$inherit_activity->set_id( $activity->get_id() );
$inherit_activity->set_cc( $activity->get_cc() );
$inherit_activity->set_to( $activity->get_to() );

return $inherit_activity;
}

return $activity;
}
}
69 changes: 60 additions & 9 deletions includes/class-move.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Activitypub;

use Activitypub\Activity\Actor;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Actors;

/**
Expand All @@ -17,14 +18,20 @@
*/
class Move {
/**
* Move an ActivityPub account from one location to another.
* External Move.
*
* Move an ActivityPub Actor from one location (internal) to another (external).
*
* This helps migrating local profiles to a new external profile:
*
* `Move::externally( 'https://example.com/?author=123', 'https://mastodon.example/users/foo' );`
*
* @param string $from The current account URL.
* @param string $to The new account URL.
*
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
*/
public static function account( $from, $to ) {
public static function externally( $from, $to ) {
$user = Actors::get_by_various( $from );

if ( \is_wp_error( $user ) ) {
Expand All @@ -38,13 +45,6 @@ public static function account( $from, $to ) {
\update_option( 'activitypub_blog_user_moved_to', $to );
}

// Add the old account URL to alsoKnownAs.
if ( $user->get__id() > 0 ) {
self::update_user_also_known_as( $user->get__id(), $from );
} else {
self::update_blog_also_known_as( $from );
}

$response = Http::get_remote_object( $to );

if ( \is_wp_error( $response ) ) {
Expand All @@ -64,6 +64,57 @@ public static function account( $from, $to ) {
return add_to_outbox( $actor, 'Move', $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
}

/**
* Internal Move.
*
* Move an ActivityPub Actor from one location (internal) to another (internal).
*
* This helps migrating abandoned profiles to `Move` to other profiles:
*
* `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );`
*
* ... or to change Actor-IDs like:
*
* `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );`
*
* @param string $from The current account URL.
* @param string $to The new account URL.
*
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
*/
public static function internally( $from, $to ) {
$user = Actors::get_by_various( $from );

if ( \is_wp_error( $user ) ) {
return $user;
}

// Add the old account URL to alsoKnownAs.
if ( $user->get__id() > 0 ) {
self::update_user_also_known_as( $user->get__id(), $from );
\update_user_option( $user->get__id(), 'activitypub_move_to', $to );
} else {
self::update_blog_also_known_as( $from );
\update_option( 'activitypub_blog_user_moved_to', $to );
}

// check if `$from` is a URL or an ID.
if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) {
$actor = $from;
} else {
$actor = $user->get_id();
}

$activity = new Activity();
$activity->set_type( 'Move' );
$activity->set_actor( $actor );
$activity->set_origin( $actor );
$activity->set_object( $to );
$activity->set_target( $to );

return add_to_outbox( $activity, 'Inherit', $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
}

/**
* Update the alsoKnownAs property of a user.
*
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ For reasons of data protection, it is not possible to see the followers of other
= Unreleased =

* Added: Documentation for migrating from a Mastodon instance to WordPress.
* Added: Introduce `Inherit` as a valid activity type, to store full Activities as main object.
* Changed: Outbox items only get sent to followers when there are any.
* Fixed: Updates to certain user meta fields did not trigger an Update activity.
* Fixed: When viewing Reply Contexts, we'll now attribute the post to the blog user when the post author is disabled.
Expand Down
60 changes: 60 additions & 0 deletions tests/includes/class-test-handler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php
/**
* Handler Test Class
*
* @package Activitypub
*/

namespace Activitypub\Tests;

use Activitypub\Activity\Activity;
use Activitypub\Collection\Outbox;
use Activitypub\Handler;
use WP_UnitTestCase;

use function Activitypub\add_to_outbox;

/**
* Handler Test Class
*/
class Test_Handler extends WP_UnitTestCase {

/**
* The user ID.
*
* @var int
*/
protected $user_id;

/**
* Set up the test.
*/
public function set_up() {
parent::set_up();
$this->user_id = self::factory()->user->create(
array(
'role' => 'administrator',
)
);
}

/**
* Test the inherit functionality
*/
public function test_inherit_activity() {
// Create a mock inherit activity.
$activity = new Activity();
$activity->set_type( 'Move' );
$activity->set_content( 'Test content' );
$activity->set_id( 'https://example.com/activity/1' );
$activity->set_to( array( 'https://example.com/to' ) );
$activity->set_cc( array( 'https://example.com/cc' ) );

$id = add_to_outbox( $activity, 'Inherit', $this->user_id );

$outbox_item = get_post( $id );
$outbox_activity = Outbox::get_activity( $outbox_item );

$this->assertEquals( 'Move', $outbox_activity->get_type() );
}
}
46 changes: 32 additions & 14 deletions tests/includes/class-test-move.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ public function test_account_with_valid_input() {
$from = Actors::get_by_id( self::$user_id )->get_id();
$to = 'https://newsite.com/user/1';

\Activitypub\Move::account( $from, $to );
\Activitypub\Move::externally( $from, $to );

$moved_to = Actors::get_by_id( self::$user_id )->get_moved_to();
$this->assertEquals( $to, $moved_to );

$also_known_as = Actors::get_by_id( self::$user_id )->get_also_known_as();
$this->assertContains( $from, $also_known_as );
$moved_to = Actors::get_by_id( self::$user_id )->get_moved_to();
$this->assertEquals( $to, $moved_to );
}

/**
Expand All @@ -62,7 +62,7 @@ public function test_account_with_valid_input() {
* @covers ::account
*/
public function test_account_with_invalid_user() {
$result = \Activitypub\Move::account(
$result = \Activitypub\Move::externally(
'https://example.com/nonexistent/user',
'https://newsite.com/user/999'
);
Expand All @@ -85,7 +85,7 @@ public function test_account_with_invalid_target() {
};
\add_filter( 'pre_http_request', $filter );

$result = \Activitypub\Move::account( $from, $to );
$result = \Activitypub\Move::externally( $from, $to );

$this->assertWPError( $result );
$this->assertEquals( 'http_request_failed', $result->get_error_code() );
Expand All @@ -112,12 +112,10 @@ public function test_account_with_duplicate_moves() {
};
\add_filter( 'pre_http_request', $filter );

\Activitypub\Move::account( $from, $to );
\Activitypub\Move::externally( $from, $to );

$also_known_as = Actors::get_by_id( self::$user_id )->get_also_known_as();
$this->assertCount( 3, $also_known_as );
$this->assertContains( $from, $also_known_as );
$this->assertContains( 'https://old.example.com/user/1', $also_known_as );
$moved_to = Actors::get_by_id( self::$user_id )->get_moved_to();
$this->assertEquals( $to, $moved_to );

\remove_filter( 'pre_http_request', $filter );
}
Expand All @@ -134,12 +132,32 @@ public function test_account_with_blog_author_as_actor() {
$from = Actors::get_by_id( Actors::BLOG_USER_ID )->get_id();
$to = 'https://newsite.com/user/0';

\Activitypub\Move::account( $from, $to );
\Activitypub\Move::externally( $from, $to );

$also_known_as = Actors::get_by_id( Actors::BLOG_USER_ID )->get_also_known_as();
$this->assertCount( 3, $also_known_as );
$this->assertContains( $from, $also_known_as );
$moved_to = Actors::get_by_id( Actors::BLOG_USER_ID )->get_moved_to();
$this->assertEquals( $to, $moved_to );

\delete_option( 'activitypub_actor_mode' );
}

/**
* Test the internally() method with valid input.
*
* @covers ::internally
*/
public function test_internally_with_valid_input() {
$from = get_author_posts_url( self::$user_id );
$to = Actors::get_by_id( self::$user_id )->get_id();

\Activitypub\Move::internally( $from, $to );

// Clear cache.
wp_cache_delete( self::$user_id, 'users' );

$moved_to = Actors::get_by_id( self::$user_id )->get_moved_to();
$this->assertEquals( $to, $moved_to );

$also_known_as = Actors::get_by_id( self::$user_id )->get_also_known_as();
$this->assertContains( $from, $also_known_as );
}
}
Loading