' . \esc_html( $error_message ) . ' ';
\printf(
/* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */
\esc_html__( 'File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your %1$s file or by %2$s being defined as smaller than %3$s in %1$s.', 'activitypub' ),
'php.ini',
'post_max_size',
'upload_max_filesize'
);
echo '';
return false;
}
$file_info = \wp_check_filetype( \sanitize_file_name( $_FILES['import']['name'] ), array( 'json' => 'application/json' ) );
if ( 'application/json' !== $file_info['type'] ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'The uploaded file must be a JSON file. Please try again with the correct file format.', 'activitypub' ) );
return false;
}
$overrides = array(
'test_form' => false,
'test_type' => false,
);
$upload = \wp_handle_upload( $_FILES['import'], $overrides );
if ( isset( $upload['error'] ) ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html( $upload['error'] ) );
return false;
}
// Construct the attachment array.
$attachment = array(
'post_title' => \wp_basename( $upload['file'] ),
'post_content' => $upload['url'],
'post_mime_type' => $upload['type'],
'guid' => $upload['url'],
'context' => 'import',
'post_status' => 'private',
);
// Save the data.
self::$import_id = \wp_insert_attachment( $attachment, $upload['file'] );
// Schedule a cleanup for one day from now in case of failed import or missing wp_import_cleanup() call.
\wp_schedule_single_event( \time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( self::$import_id ) );
return true;
}
/**
* Handle URL import.
*
* @return bool Whether the import was started.
*/
public static function handle_url_import() {
$error_message = \__( 'Sorry, there has been an error.', 'activitypub' );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$url = \sanitize_url( \wp_unslash( $_POST['import_url'] ?? '' ) );
if ( empty( $url ) ) {
echo '' . \esc_html( $error_message ) . ' ';
echo \esc_html__( 'Please provide a valid URL.', 'activitypub' ) . '
';
return false;
}
// Validate URL format.
if ( ! \filter_var( $url, FILTER_VALIDATE_URL ) ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'The provided URL is not valid.', 'activitypub' ) );
return false;
}
// Fetch the URL content. Http::get() sends signed requests with the ActivityPub
// headers, so starter kits hosted on servers that require Authorized Fetch work too.
$response = Http::get(
$url,
array(
'timeout' => 30,
'redirection' => 5,
// A curated starter-kit collection can exceed Http::get()'s default 1 MiB cap.
'limit_response_size' => 25 * MB_IN_BYTES,
)
);
if ( \is_wp_error( $response ) ) {
// Http::get() reports a failed HTTP response as a WP_Error whose data carries the status code.
$status = $response->get_error_data();
$status = \is_array( $status ) && ! empty( $status['status'] ) ? (int) $status['status'] : 0;
$message = $status
/* translators: %d: HTTP response code */
? \sprintf( \__( 'Failed to fetch URL. HTTP response code: %d', 'activitypub' ), $status )
: $response->get_error_message();
\printf( '%s %s
', \esc_html( $error_message ), \esc_html( $message ) );
return false;
}
$body = \wp_remote_retrieve_body( $response );
if ( empty( $body ) ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'The URL returned empty content.', 'activitypub' ) );
return false;
}
// Validate JSON format.
$json_data = \json_decode( $body, true );
if ( null === $json_data ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'The URL does not contain valid JSON data.', 'activitypub' ) );
return false;
}
// Create a temporary file to store the JSON content.
$upload_dir = \wp_upload_dir();
$base_filename = 'starter-kit.json';
$unique_filename = \wp_unique_filename( $upload_dir['path'], $base_filename );
$temp_file = \trailingslashit( $upload_dir['path'] ) . $unique_filename;
if ( ! \WP_Filesystem() ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'Failed to initialize the WordPress filesystem.', 'activitypub' ) );
return false;
}
global $wp_filesystem;
if ( ! $wp_filesystem || ! \is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'Failed to initialize the WordPress filesystem.', 'activitypub' ) );
return false;
}
if ( ! $wp_filesystem->put_contents( $temp_file, $body, FS_CHMOD_FILE ) ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'Failed to save the downloaded content.', 'activitypub' ) );
return false;
}
// Construct the attachment array.
$attachment = array(
// phpcs:ignore
'post_title' => \sanitize_file_name( \basename( \wp_parse_url( $url, PHP_URL_PATH ) ) ) ?: 'starter-kit.json',
'post_content' => $url,
'post_mime_type' => 'application/json',
'guid' => $url,
'context' => 'import',
'post_status' => 'private',
);
// Save the data.
self::$import_id = \wp_insert_attachment( $attachment, $temp_file );
// Check if the attachment was inserted successfully.
if ( \is_wp_error( self::$import_id ) || ! self::$import_id ) {
\printf( '%s %s
', \esc_html( $error_message ), \esc_html__( 'Failed to insert attachment.', 'activitypub' ) );
return false;
}
// Schedule a cleanup for one day from now in case of failed import or missing wp_import_cleanup() call.
\wp_schedule_single_event( \time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( self::$import_id ) );
return true;
}
/**
* Import options.
*
* @return void
*/
public static function import_options() {
self::setup_blog_user_filter();
$actors = self::get_actor_list();
if ( \is_wp_error( $actors ) ) {
self::render_error( $actors );
return;
}
self::render_import_form( $actors );
self::cleanup_blog_user_filter();
}
/**
* Setup blog user filter for dropdown.
*
* @return void
*/
private static function setup_blog_user_filter() {
if ( is_user_type_disabled( 'blog' ) ) {
return;
}
self::$blog_user_filter_callback = static function ( $users ) {
return \preg_replace(
'/<\/select>/',
'' . \__( 'Blog User', 'activitypub' ) . ' ',
$users
);
};
\add_filter( 'wp_dropdown_users', self::$blog_user_filter_callback );
self::$blog_user_filter_added = true;
}
/**
* Cleanup blog user filter.
*
* @return void
*/
private static function cleanup_blog_user_filter() {
if ( self::$blog_user_filter_callback && self::$blog_user_filter_added ) {
\remove_filter( 'wp_dropdown_users', self::$blog_user_filter_callback );
self::$blog_user_filter_callback = null;
}
self::$blog_user_filter_added = false;
}
/**
* Render error message.
*
* @param \WP_Error $error The error to render.
*
* @return void
*/
private static function render_error( $error ) {
\printf(
'%s %s
',
\esc_html__( 'Sorry, there has been an error.', 'activitypub' ),
\esc_html( $error->get_error_message() )
);
}
/**
* Render the import form.
*
* @param array $actors The actors to render.
*
* @return void
*/
private static function render_import_form( $actors ) {
?>
' . \esc_html( $name ) . '';
if ( ! empty( self::$starter_kit['image'] ) ) {
$image_url = object_to_uri( self::$starter_kit['image'] );
if ( $image_url ) {
\printf(
' ',
\esc_url( $image_url ),
\esc_attr( $name )
);
}
}
if ( ! empty( self::$starter_kit['summary'] ) ) {
echo '' . \esc_html( self::$starter_kit['summary'] ) . '
';
}
if ( ! empty( self::$starter_kit['attributedTo'] ) ) {
echo \wp_kses_post(
\sprintf(
'Created by %1$s ',
\esc_url( self::$starter_kit['attributedTo'] )
)
);
}
}
/**
* Render author selection.
*
* @return void
*/
private static function render_author_selection() {
?>
'author',
'id' => 'author',
'show' => 'display_name_with_login',
'selected' => \get_current_user_id(),
'capability' => 'activitypub',
)
);
?>
%s %s', \esc_html( $error_message ), \esc_html( $result->get_error_message() ) );
} else {
\printf( '%s
', \esc_html__( 'All done.', 'activitypub' ) );
}
/**
* Fires when the Starter Kit import ends.
*/
\do_action( 'import_end' );
}
/**
* Process posts.
*
* @return true|\WP_Error True on success, WP_Error on failure.
*/
public static function follow() {
$skipped = 0;
$followed = 0;
$items = self::$actor_list;
foreach ( $items as $actor_id ) {
$actor_id = object_to_uri( $actor_id );
$actor_id = \ltrim( $actor_id, '@' );
if ( ! \filter_var( $actor_id, FILTER_VALIDATE_URL ) && ! \filter_var( $actor_id, FILTER_VALIDATE_EMAIL ) ) {
++$skipped;
continue;
}
$result = follow( $actor_id, self::$author );
if ( \is_wp_error( $result ) ) {
/* translators: %s: Account ID */
\printf( '' . \esc_html__( '✗ %s', 'activitypub' ) . '
', \esc_html( $actor_id ) );
++$skipped;
} else {
/* translators: %s: Account ID */
\printf( '' . \esc_html__( '✓ %s', 'activitypub' ) . '
', \esc_html( $actor_id ) );
++$followed;
}
}
echo ' ';
/* translators: %d: Number of followed actors */
\printf( '%s
', \esc_html( \sprintf( \_n( 'Followed %s Actor.', 'Followed %s Actors.', $followed, 'activitypub' ), \number_format_i18n( $followed ) ) ) );
/* translators: %d: Number of skipped items */
\printf( '%s
', \esc_html( \sprintf( \_n( 'Skipped %s Item.', 'Skipped %s Items.', $skipped, 'activitypub' ), \number_format_i18n( $skipped ) ) ) );
return true;
}
/**
* Intro.
*
* @return void
*/
public static function greet() {
echo '';
echo '
' . \esc_html__( 'Starter Kits use the ActivityPub protocol with custom extensions to automate tasks such as following accounts, blocking unwanted content, and applying default configurations. The importer will automatically follow every user listed in the kit, helping users connect right away. Support for additional actions and features will be added over time.', 'activitypub' ) . '
';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$url = isset( $_GET['url'] ) ? \sanitize_text_field( \wp_unslash( $_GET['url'] ) ) : '';
if ( empty( $url ) ) {
// File upload option.
\printf( '
%s ', \esc_html__( 'Option 1: Upload a File', 'activitypub' ) );
\wp_import_upload_form( 'admin.php?import=starter-kit&step=1' );
// URL import option.
\printf( '
%s ', \esc_html__( 'Option 2: Import from URL', 'activitypub' ) );
} else {
// URL import option.
\printf( '
%s ', \esc_html__( 'Import from URL', 'activitypub' ) );
}
?>
';
echo '' . \esc_html__( 'Import a Fediverse Starter Kit (Beta)', 'activitypub' ) . ' ';
}
/**
* Footer.
*
* @return void
*/
public static function footer() {
echo '';
}
/**
* Get actor list.
*
* @return array|\WP_Error The actors, or WP_Error when the file cannot be used.
*/
private static function get_actor_list() {
$file = \get_attached_file( self::$import_id );
\WP_Filesystem();
global $wp_filesystem;
$file_contents = $wp_filesystem->get_contents( $file );
if ( false === $file_contents ) {
return new \WP_Error( 'file_not_found', \esc_html__( 'Could not read the uploaded file.', 'activitypub' ) );
}
self::$starter_kit = \json_decode( $file_contents, true );
if ( null === self::$starter_kit ) {
return new \WP_Error( 'invalid_json', \esc_html__( 'Invalid JSON format in the uploaded file.', 'activitypub' ) );
}
/*
* Validate that the type is a Collection-like type.
* FeaturedCollection is from the Mastodon FEP draft:
* https://github.com/mastodon/featured_collections/pull/1
*/
$type = (array) ( self::$starter_kit['type'] ?? array() );
$valid_types = array( 'Collection', 'OrderedCollection', 'FeaturedCollection' );
if ( ! \array_intersect( $type, $valid_types ) ) {
return new \WP_Error( 'invalid_type', \esc_html__( 'The file does not contain a valid Starter Kit Collection.', 'activitypub' ) );
}
$actors = self::$starter_kit['items'] ?? self::$starter_kit['orderedItems'] ?? array();
// Limit list to 150 actors.
// TODO: Make this configurable.
$actors = \array_slice( $actors, 0, 150 );
if ( ! $actors ) {
return new \WP_Error( 'empty_actor_list', \esc_html__( 'The uploaded file does not contain any actors.', 'activitypub' ) );
}
return $actors;
}
}