id === 'settings_page_activitypub' ) {
$this->user_id = Actors::BLOG_USER_ID;
} else {
$this->user_id = \get_current_user_id();
\add_action( 'admin_notices', array( $this, 'process_admin_notices' ) );
}
parent::__construct(
array(
'singular' => \__( 'Following', 'activitypub' ),
'plural' => \__( 'Followings', 'activitypub' ),
'ajax' => false,
)
);
\add_action( 'load-' . \get_current_screen()->id, array( $this, 'process_action' ), 20 );
}
/**
* Process action.
*
* @return void
*/
public function process_action() {
if ( ! \current_user_can( 'edit_user', $this->user_id ) ) {
return;
}
if ( ! $this->current_action() ) {
return;
}
$redirect_to = \add_query_arg(
array(
'settings-updated' => true, // Tell WordPress to load settings errors transient.
'action' => false, // Remove action parameter to prevent redirect loop.
)
);
switch ( $this->current_action() ) {
case 'delete':
$redirect_to = \remove_query_arg( array( 'follower', 'following' ), $redirect_to );
// Handle single follower deletion.
if ( isset( $_GET['follower'], $_GET['_wpnonce'] ) ) {
$follower = \absint( $_GET['follower'] );
$nonce = \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) );
if ( \wp_verify_nonce( $nonce, 'delete-follower_' . $follower ) ) {
Following_Collection::unfollow( $follower, $this->user_id );
\add_settings_error( 'activitypub', 'follower_deleted', \__( 'Account unfollowed.', 'activitypub' ), 'success' );
}
}
// Handle bulk actions.
if ( isset( $_REQUEST['following'], $_REQUEST['_wpnonce'] ) ) {
$nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( \wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
$following = \array_map( 'absint', \wp_unslash( $_REQUEST['following'] ) );
foreach ( $following as $post_id ) {
Following_Collection::unfollow( $post_id, $this->user_id );
}
$count = \count( $following );
/* translators: %d: Number of accounts unfollowed. */
$message = \_n( '%d account unfollowed.', '%d accounts unfollowed.', $count, 'activitypub' );
$message = \sprintf( $message, \number_format_i18n( $count ) );
\add_settings_error( 'activitypub', 'followers_deleted', $message, 'success' );
}
}
break;
case 'follow':
$redirect_to = \remove_query_arg( array( 'resource', 's' ), $redirect_to );
if ( ! isset( $_REQUEST['activitypub-profile'], $_REQUEST['_wpnonce'] ) ) {
return;
}
$nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( ! \wp_verify_nonce( $nonce, 'activitypub-follow-nonce' ) ) {
return;
}
$original = \sanitize_text_field( \wp_unslash( $_REQUEST['activitypub-profile'] ) );
$profile = Remote_Actors::normalize_identifier( $original );
if ( ! $profile ) {
/* translators: %s: Account profile that could not be followed */
\add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account “%s”. Please verify the account exists and try again.', 'activitypub' ), \esc_html( $original ) ) );
$redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
break;
}
// Check if actor is blocked.
if ( Moderation::is_actor_blocked( $profile, $this->user_id ) ) {
/* translators: %s: Account profile that could not be followed */
\add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account “%s”. The account is blocked.', 'activitypub' ), \esc_html( $profile ) ) );
$redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
break;
}
// Check if already following before making the request.
$actor = Remote_Actors::get_by_uri( $profile );
if ( ! \is_wp_error( $actor ) ) {
$following = \get_post_meta( $actor->ID, Following_Collection::FOLLOWING_META_KEY, false );
$pending = \get_post_meta( $actor->ID, Following_Collection::PENDING_META_KEY, false );
if ( \in_array( (string) $this->user_id, $following, true ) ) {
/* translators: %s: Account profile that is already being followed */
\add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'You are already following “%s”.', 'activitypub' ), \esc_html( $profile ) ), 'info' );
break;
} elseif ( \in_array( (string) $this->user_id, $pending, true ) ) {
/* translators: %s: Account profile that has a pending follow request */
\add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'You already have a pending follow request for “%s”.', 'activitypub' ), \esc_html( $profile ) ), 'info' );
break;
}
}
$result = follow( $profile, $this->user_id );
if ( \is_wp_error( $result ) ) {
/* translators: %s: Account profile that could not be followed */
\add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account “%s”. Please verify the account exists and try again.', 'activitypub' ), \esc_html( $profile ) ) );
$redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
} else {
\add_settings_error( 'activitypub', 'followed', \__( 'Account followed.', 'activitypub' ), 'success' );
}
break;
default:
break;
}
\set_transient( 'settings_errors', \get_settings_errors(), 30 ); // 30 seconds.
\wp_safe_redirect( $redirect_to );
exit;
}
/**
* Process admin notices based on query parameters.
*
* @return void
*/
public function process_admin_notices() {
\settings_errors( 'activitypub' );
}
/**
* Prepare items.
*
* @return void
*/
public function prepare_items() {
$status = Following_Collection::ALL;
$page_num = $this->get_pagenum();
$per_page = $this->get_items_per_page( 'activitypub_following_per_page' );
$args = array();
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['orderby'] ) ) {
$args['orderby'] = \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) );
}
if ( isset( $_GET['order'] ) ) {
$args['order'] = \sanitize_text_field( \wp_unslash( $_GET['order'] ) );
}
if ( isset( $_GET['s'] ) ) {
$args['s'] = $this->normalize_search_term( \wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
}
if ( isset( $_GET['status'] ) ) {
$status = \sanitize_text_field( \wp_unslash( $_GET['status'] ) );
}
if ( Following_Collection::PENDING === $status ) {
$following_with_count = Following_Collection::query_pending( $this->user_id, $per_page, $page_num, $args );
} elseif ( Following_Collection::ACCEPTED === $status ) {
$following_with_count = Following_Collection::query( $this->user_id, $per_page, $page_num, $args );
} else {
$following_with_count = Following_Collection::query_all( $this->user_id, $per_page, $page_num, $args );
}
$followings = $following_with_count['following'];
$counter = $following_with_count['total'];
$this->items = array();
$this->set_pagination_args(
array(
'total_items' => (int) $counter,
'total_pages' => (int) \ceil( $counter / $per_page ),
'per_page' => $per_page,
)
);
foreach ( $followings as $following ) {
$actor = Remote_Actors::get_actor( $following );
if ( \is_wp_error( $actor ) ) {
continue;
}
$this->items[] = array(
'id' => $following->ID,
'icon' => object_to_uri( $actor->get_icon() ?? ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg' ),
'post_title' => $actor->get_name() ?: $actor->get_preferred_username(),
'username' => $actor->get_preferred_username(),
'url' => object_to_uri( $actor->get_url() ?: $actor->get_id() ),
'webfinger' => $actor->get_webfinger(),
'status' => Following_Collection::check_status( $this->user_id, $following->ID ),
'identifier' => $actor->get_id(),
'modified' => $following->post_modified_gmt,
);
}
}
/**
* Returns views.
*
* @return string[]
*/
public function get_views() {
$count = Following_Collection::count_by_status( $this->user_id );
$path = 'users.php?page=activitypub-following-list';
$status = Following_Collection::ALL;
if ( Actors::BLOG_USER_ID === $this->user_id ) {
$path = 'options-general.php?page=activitypub&tab=following';
}
if ( ! empty( $_GET['status'] ) ) {
$status = \sanitize_text_field( \wp_unslash( $_GET['status'] ) );
}
$links = array(
'all' => array(
'url' => \admin_url( $path ),
'label' => \sprintf(
/* translators: %s: Number of users. */
\_nx(
'All (%s)',
'All (%s)',
$count[ Following_Collection::ALL ],
'users',
'activitypub'
),
\number_format_i18n( $count[ Following_Collection::ALL ] )
),
'current' => Following_Collection::ALL === $status,
),
'accepted' => array(
'url' => \admin_url( $path . '&status=' . Following_Collection::ACCEPTED ),
'label' => \sprintf(
/* translators: %s: Number of users. */
\_nx(
'Accepted (%s)',
'Accepted (%s)',
$count[ Following_Collection::ACCEPTED ],
'users',
'activitypub'
),
\number_format_i18n( $count[ Following_Collection::ACCEPTED ] )
),
'current' => Following_Collection::ACCEPTED === $status,
),
'pending' => array(
'url' => \admin_url( $path . '&status=' . Following_Collection::PENDING ),
'label' => \sprintf(
/* translators: %s: Number of users. */
\_nx(
'Pending (%s)',
'Pending (%s)',
$count[ Following_Collection::PENDING ],
'users',
'activitypub'
),
\number_format_i18n( $count[ Following_Collection::PENDING ] )
),
'current' => Following_Collection::PENDING === $status,
),
);
return $this->get_views_links( $links );
}
/**
* Returns bulk actions.
*
* @return array
*/
public function get_bulk_actions() {
return array(
'delete' => \__( 'Unfollow', 'activitypub' ),
);
}
/**
* Column avatar.
*
* @param array $item Item.
* @return string
*/
public function column_cb( $item ) {
return \sprintf( '', \esc_attr( $item['id'] ) );
}
/**
* Column url.
*
* @param array $item Item.
* @return string
*/
public function column_username( $item ) {
$status = '';
if (
( ! isset( $_GET['status'] ) || Following_Collection::ALL === $_GET['status'] ) &&
( Following_Collection::PENDING === $item['status'] )
) {
$status = \sprintf( ' — %s', \esc_html__( 'Pending', 'activitypub' ) );
}
return \sprintf(
' %4$s%5$s
',
\esc_url( $item['icon'] ),
\esc_attr( $item['post_title'] ),
\esc_url( $item['url'] ),
\esc_html( $item['username'] ),
$status
);
}
/**
* Column WebFinger.
*
* @param array $item Item.
*
* @return string The WebFinger link.
*/
public function column_webfinger( $item ) {
$webfinger = Sanitize::webfinger( $item['webfinger'] );
return \sprintf(
'@%2$s',
\esc_url( $item['url'] ),
\esc_html( $webfinger )
);
}
/**
* Column modified.
*
* @param array $item Item.
* @return string
*/
public function column_modified( $item ) {
$modified = \strtotime( $item['modified'] );
return \sprintf(
'',
\esc_attr( \gmdate( 'c', $modified ) ),
\esc_html( \gmdate( \get_option( 'date_format' ), $modified ) )
);
}
/**
* Message to be displayed when there are no followings.
*
* @return void
*/
public function no_items() {
\esc_html_e( 'No profiles found.', 'activitypub' );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$search = \sanitize_text_field( \wp_unslash( $_GET['s'] ?? '' ) );
if ( empty( $search ) ) {
return;
}
$search = Sanitize::webfinger( $search );
if ( \filter_var( $search, FILTER_VALIDATE_EMAIL ) ) {
return;
}
$search = Webfinger::resolve( $search );
if ( ! \is_wp_error( $search ) && \filter_var( $search, FILTER_VALIDATE_URL ) ) {
$actor = Remote_Actors::fetch_by_uri( $search );
if ( ! \is_wp_error( $actor ) ) {
echo ' ';
\printf(
/* translators: %s: Actor name. */
\esc_html__( 'Would you like to follow %s?', 'activitypub' ),
\sprintf(
'%s',
\esc_url( \add_query_arg( 'resource', $search ) ),
\esc_html( $actor->post_title )
)
);
}
}
}
/**
* Single row.
*
* @param array $item Item.
*
* @return void
*/
public function single_row( $item ) {
\printf(
'