| 1 |
<?php |
| 2 |
/** |
| 3 |
* Starter Kit importer file. |
| 4 |
* |
| 5 |
* @package Activitypub |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Activitypub\WP_Admin\Import; |
| 9 |
|
| 10 |
use Activitypub\Http; |
| 11 |
|
| 12 |
use function Activitypub\follow; |
| 13 |
use function Activitypub\is_user_type_disabled; |
| 14 |
use function Activitypub\object_to_uri; |
| 15 |
|
| 16 |
/** |
| 17 |
* Starter Kit importer class. |
| 18 |
*/ |
| 19 |
class Starter_Kit { |
| 20 |
/** |
| 21 |
* Import file attachment ID. |
| 22 |
* |
| 23 |
* @var int |
| 24 |
*/ |
| 25 |
private static $import_id; |
| 26 |
|
| 27 |
/** |
| 28 |
* Author ID. |
| 29 |
* |
| 30 |
* @var int |
| 31 |
*/ |
| 32 |
private static $author; |
| 33 |
|
| 34 |
/** |
| 35 |
* Starter Kit file. |
| 36 |
* |
| 37 |
* @var string |
| 38 |
*/ |
| 39 |
private static $file; |
| 40 |
|
| 41 |
/** |
| 42 |
* Starter Kit JSON. |
| 43 |
* |
| 44 |
* @var object |
| 45 |
*/ |
| 46 |
private static $starter_kit; |
| 47 |
|
| 48 |
/** |
| 49 |
* Actors to follow. |
| 50 |
* |
| 51 |
* @var array |
| 52 |
*/ |
| 53 |
private static $actor_list; |
| 54 |
|
| 55 |
/** |
| 56 |
* Blog user filter callback. |
| 57 |
* |
| 58 |
* @var callable |
| 59 |
*/ |
| 60 |
private static $blog_user_filter_callback; |
| 61 |
|
| 62 |
/** |
| 63 |
* Blog user filter added. |
| 64 |
* |
| 65 |
* @var bool |
| 66 |
*/ |
| 67 |
private static $blog_user_filter_added = false; |
| 68 |
|
| 69 |
/** |
| 70 |
* Dispatch |
| 71 |
*/ |
| 72 |
public static function dispatch() { |
| 73 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 74 |
$step = \absint( $_GET['step'] ?? 0 ); |
| 75 |
|
| 76 |
self::header(); |
| 77 |
|
| 78 |
switch ( $step ) { |
| 79 |
case 0: |
| 80 |
self::greet(); |
| 81 |
break; |
| 82 |
|
| 83 |
case 1: |
| 84 |
\check_admin_referer( 'import-upload' ); |
| 85 |
if ( self::handle_upload() ) { |
| 86 |
self::import_options(); |
| 87 |
} |
| 88 |
break; |
| 89 |
|
| 90 |
case 2: |
| 91 |
\check_admin_referer( 'import-url' ); |
| 92 |
if ( self::handle_url_import() ) { |
| 93 |
self::import_options(); |
| 94 |
} |
| 95 |
break; |
| 96 |
|
| 97 |
case 3: |
| 98 |
\check_admin_referer( 'import-starter-kit' ); |
| 99 |
self::$import_id = \absint( $_POST['import_id'] ?? 0 ); |
| 100 |
self::$author = \absint( $_POST['author'] ?? \get_current_user_id() ); |
| 101 |
self::$actor_list = \array_values( |
| 102 |
\array_filter( |
| 103 |
\array_map( |
| 104 |
static function ( $actor ) { |
| 105 |
$actor = \sanitize_text_field( $actor ); |
| 106 |
$actor = \wp_unslash( $actor ); |
| 107 |
return self::is_valid_actor( $actor ) ? $actor : null; |
| 108 |
}, |
| 109 |
// phpcs:ignore |
| 110 |
$_POST['actors'] ?? array() |
| 111 |
) |
| 112 |
) |
| 113 |
); |
| 114 |
|
| 115 |
\set_time_limit( 0 ); |
| 116 |
self::import(); |
| 117 |
break; |
| 118 |
} |
| 119 |
|
| 120 |
self::footer(); |
| 121 |
} |
| 122 |
|
| 123 |
/** |
| 124 |
* Handle upload. |
| 125 |
*/ |
| 126 |
public static function handle_upload() { |
| 127 |
$error_message = \__( 'Sorry, there has been an error.', 'activitypub' ); |
| 128 |
|
| 129 |
\check_admin_referer( 'import-upload' ); |
| 130 |
|
| 131 |
if ( ! isset( $_FILES['import']['name'] ) ) { |
| 132 |
echo '<p><strong>' . \esc_html( $error_message ) . '</strong><br />'; |
| 133 |
\printf( |
| 134 |
/* translators: 1: php.ini, 2: post_max_size, 3: upload_max_filesize */ |
| 135 |
\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' ), |
| 136 |
'php.ini', |
| 137 |
'post_max_size', |
| 138 |
'upload_max_filesize' |
| 139 |
); |
| 140 |
echo '</p>'; |
| 141 |
return false; |
| 142 |
} |
| 143 |
|
| 144 |
$file_info = \wp_check_filetype( \sanitize_file_name( $_FILES['import']['name'] ), array( 'json' => 'application/json' ) ); |
| 145 |
if ( 'application/json' !== $file_info['type'] ) { |
| 146 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'The uploaded file must be a JSON file. Please try again with the correct file format.', 'activitypub' ) ); |
| 147 |
return false; |
| 148 |
} |
| 149 |
|
| 150 |
$overrides = array( |
| 151 |
'test_form' => false, |
| 152 |
'test_type' => false, |
| 153 |
); |
| 154 |
|
| 155 |
$upload = \wp_handle_upload( $_FILES['import'], $overrides ); |
| 156 |
|
| 157 |
if ( isset( $upload['error'] ) ) { |
| 158 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html( $upload['error'] ) ); |
| 159 |
return false; |
| 160 |
} |
| 161 |
|
| 162 |
// Construct the attachment array. |
| 163 |
$attachment = array( |
| 164 |
'post_title' => \wp_basename( $upload['file'] ), |
| 165 |
'post_content' => $upload['url'], |
| 166 |
'post_mime_type' => $upload['type'], |
| 167 |
'guid' => $upload['url'], |
| 168 |
'context' => 'import', |
| 169 |
'post_status' => 'private', |
| 170 |
); |
| 171 |
|
| 172 |
// Save the data. |
| 173 |
self::$import_id = \wp_insert_attachment( $attachment, $upload['file'] ); |
| 174 |
|
| 175 |
// Schedule a cleanup for one day from now in case of failed import or missing wp_import_cleanup() call. |
| 176 |
\wp_schedule_single_event( \time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( self::$import_id ) ); |
| 177 |
|
| 178 |
return true; |
| 179 |
} |
| 180 |
|
| 181 |
/** |
| 182 |
* Handle URL import. |
| 183 |
*/ |
| 184 |
public static function handle_url_import() { |
| 185 |
$error_message = \__( 'Sorry, there has been an error.', 'activitypub' ); |
| 186 |
|
| 187 |
// phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 188 |
$url = \sanitize_url( \wp_unslash( $_POST['import_url'] ?? '' ) ); |
| 189 |
if ( empty( $url ) ) { |
| 190 |
echo '<p><strong>' . \esc_html( $error_message ) . '</strong><br />'; |
| 191 |
echo \esc_html__( 'Please provide a valid URL.', 'activitypub' ) . '</p>'; |
| 192 |
return false; |
| 193 |
} |
| 194 |
|
| 195 |
// Validate URL format. |
| 196 |
if ( ! \filter_var( $url, FILTER_VALIDATE_URL ) ) { |
| 197 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'The provided URL is not valid.', 'activitypub' ) ); |
| 198 |
return false; |
| 199 |
} |
| 200 |
|
| 201 |
// Fetch the URL content. Http::get() sends signed requests with the ActivityPub |
| 202 |
// headers, so starter kits hosted on servers that require Authorized Fetch work too. |
| 203 |
$response = Http::get( |
| 204 |
$url, |
| 205 |
array( |
| 206 |
'timeout' => 30, |
| 207 |
'redirection' => 5, |
| 208 |
// A curated starter-kit collection can exceed Http::get()'s default 1 MiB cap. |
| 209 |
'limit_response_size' => 25 * MB_IN_BYTES, |
| 210 |
) |
| 211 |
); |
| 212 |
|
| 213 |
if ( \is_wp_error( $response ) ) { |
| 214 |
// Http::get() reports a failed HTTP response as a WP_Error whose data carries the status code. |
| 215 |
$status = $response->get_error_data(); |
| 216 |
$status = \is_array( $status ) && ! empty( $status['status'] ) ? (int) $status['status'] : 0; |
| 217 |
$message = $status |
| 218 |
/* translators: %d: HTTP response code */ |
| 219 |
? \sprintf( \__( 'Failed to fetch URL. HTTP response code: %d', 'activitypub' ), $status ) |
| 220 |
: $response->get_error_message(); |
| 221 |
|
| 222 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html( $message ) ); |
| 223 |
return false; |
| 224 |
} |
| 225 |
|
| 226 |
$body = \wp_remote_retrieve_body( $response ); |
| 227 |
if ( empty( $body ) ) { |
| 228 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'The URL returned empty content.', 'activitypub' ) ); |
| 229 |
return false; |
| 230 |
} |
| 231 |
|
| 232 |
// Validate JSON format. |
| 233 |
$json_data = \json_decode( $body, true ); |
| 234 |
if ( null === $json_data ) { |
| 235 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'The URL does not contain valid JSON data.', 'activitypub' ) ); |
| 236 |
return false; |
| 237 |
} |
| 238 |
|
| 239 |
// Create a temporary file to store the JSON content. |
| 240 |
$upload_dir = \wp_upload_dir(); |
| 241 |
$base_filename = 'starter-kit.json'; |
| 242 |
$unique_filename = \wp_unique_filename( $upload_dir['path'], $base_filename ); |
| 243 |
$temp_file = \trailingslashit( $upload_dir['path'] ) . $unique_filename; |
| 244 |
|
| 245 |
if ( ! \WP_Filesystem() ) { |
| 246 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'Failed to initialize the WordPress filesystem.', 'activitypub' ) ); |
| 247 |
return false; |
| 248 |
} |
| 249 |
|
| 250 |
global $wp_filesystem; |
| 251 |
|
| 252 |
if ( ! $wp_filesystem || ! \is_a( $wp_filesystem, 'WP_Filesystem_Base' ) ) { |
| 253 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'Failed to initialize the WordPress filesystem.', 'activitypub' ) ); |
| 254 |
return false; |
| 255 |
} |
| 256 |
if ( ! $wp_filesystem->put_contents( $temp_file, $body, FS_CHMOD_FILE ) ) { |
| 257 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'Failed to save the downloaded content.', 'activitypub' ) ); |
| 258 |
return false; |
| 259 |
} |
| 260 |
|
| 261 |
// Construct the attachment array. |
| 262 |
$attachment = array( |
| 263 |
// phpcs:ignore |
| 264 |
'post_title' => \sanitize_file_name( \basename( \wp_parse_url( $url, PHP_URL_PATH ) ) ) ?: 'starter-kit.json', |
| 265 |
'post_content' => $url, |
| 266 |
'post_mime_type' => 'application/json', |
| 267 |
'guid' => $url, |
| 268 |
'context' => 'import', |
| 269 |
'post_status' => 'private', |
| 270 |
); |
| 271 |
|
| 272 |
// Save the data. |
| 273 |
self::$import_id = \wp_insert_attachment( $attachment, $temp_file ); |
| 274 |
|
| 275 |
// Check if the attachment was inserted successfully. |
| 276 |
if ( \is_wp_error( self::$import_id ) || ! self::$import_id ) { |
| 277 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html__( 'Failed to insert attachment.', 'activitypub' ) ); |
| 278 |
return false; |
| 279 |
} |
| 280 |
// Schedule a cleanup for one day from now in case of failed import or missing wp_import_cleanup() call. |
| 281 |
\wp_schedule_single_event( \time() + DAY_IN_SECONDS, 'importer_scheduled_cleanup', array( self::$import_id ) ); |
| 282 |
|
| 283 |
return true; |
| 284 |
} |
| 285 |
|
| 286 |
/** |
| 287 |
* Import options. |
| 288 |
*/ |
| 289 |
public static function import_options() { |
| 290 |
self::setup_blog_user_filter(); |
| 291 |
|
| 292 |
$actors = self::get_actor_list(); |
| 293 |
if ( \is_wp_error( $actors ) ) { |
| 294 |
self::render_error( $actors ); |
| 295 |
return; |
| 296 |
} |
| 297 |
|
| 298 |
self::render_import_form( $actors ); |
| 299 |
self::cleanup_blog_user_filter(); |
| 300 |
} |
| 301 |
|
| 302 |
/** |
| 303 |
* Setup blog user filter for dropdown. |
| 304 |
*/ |
| 305 |
private static function setup_blog_user_filter() { |
| 306 |
if ( is_user_type_disabled( 'blog' ) ) { |
| 307 |
return; |
| 308 |
} |
| 309 |
|
| 310 |
self::$blog_user_filter_callback = static function ( $users ) { |
| 311 |
return \preg_replace( |
| 312 |
'/<\/select>/', |
| 313 |
'<option value="0">' . \__( 'Blog User', 'activitypub' ) . '</option></select>', |
| 314 |
$users |
| 315 |
); |
| 316 |
}; |
| 317 |
|
| 318 |
\add_filter( 'wp_dropdown_users', self::$blog_user_filter_callback ); |
| 319 |
|
| 320 |
self::$blog_user_filter_added = true; |
| 321 |
} |
| 322 |
|
| 323 |
/** |
| 324 |
* Cleanup blog user filter. |
| 325 |
*/ |
| 326 |
private static function cleanup_blog_user_filter() { |
| 327 |
if ( self::$blog_user_filter_callback && self::$blog_user_filter_added ) { |
| 328 |
\remove_filter( 'wp_dropdown_users', self::$blog_user_filter_callback ); |
| 329 |
self::$blog_user_filter_callback = null; |
| 330 |
} |
| 331 |
|
| 332 |
self::$blog_user_filter_added = false; |
| 333 |
} |
| 334 |
|
| 335 |
/** |
| 336 |
* Render error message. |
| 337 |
* |
| 338 |
* @param \WP_Error $error The error to render. |
| 339 |
*/ |
| 340 |
private static function render_error( $error ) { |
| 341 |
\printf( |
| 342 |
'<p><strong>%s</strong><br />%s</p>', |
| 343 |
\esc_html__( 'Sorry, there has been an error.', 'activitypub' ), |
| 344 |
\esc_html( $error->get_error_message() ) |
| 345 |
); |
| 346 |
} |
| 347 |
|
| 348 |
/** |
| 349 |
* Render the import form. |
| 350 |
* |
| 351 |
* @param array $actors The actors to render. |
| 352 |
*/ |
| 353 |
private static function render_import_form( $actors ) { |
| 354 |
?> |
| 355 |
<form action="<?php echo \esc_url( \admin_url( 'admin.php?import=starter-kit&step=3' ) ); ?>" method="post"> |
| 356 |
<?php \wp_nonce_field( 'import-starter-kit' ); ?> |
| 357 |
<input type="hidden" name="import_id" value="<?php echo \esc_attr( self::$import_id ); ?>" /> |
| 358 |
|
| 359 |
<?php self::render_starter_kit_info(); ?> |
| 360 |
<?php self::render_author_selection(); ?> |
| 361 |
<?php self::render_actor_selection( $actors ); ?> |
| 362 |
|
| 363 |
<p class="submit"> |
| 364 |
<input type="submit" class="button button-primary" value="<?php \esc_attr_e( 'Import', 'activitypub' ); ?>" /> |
| 365 |
</p> |
| 366 |
</form> |
| 367 |
<?php |
| 368 |
} |
| 369 |
|
| 370 |
/** |
| 371 |
* Render starter kit information. |
| 372 |
*/ |
| 373 |
private static function render_starter_kit_info() { |
| 374 |
$name = empty( self::$starter_kit['name'] ) |
| 375 |
? \__( 'Starter Kit', 'activitypub' ) |
| 376 |
: self::$starter_kit['name']; |
| 377 |
|
| 378 |
echo '<h3>' . \esc_html( $name ) . '</h3>'; |
| 379 |
|
| 380 |
if ( ! empty( self::$starter_kit['image'] ) ) { |
| 381 |
$image_url = object_to_uri( self::$starter_kit['image'] ); |
| 382 |
|
| 383 |
if ( $image_url ) { |
| 384 |
\printf( |
| 385 |
'<img src="%s" style="max-width: 500px;" alt="%s" />', |
| 386 |
\esc_url( $image_url ), |
| 387 |
\esc_attr( $name ) |
| 388 |
); |
| 389 |
} |
| 390 |
} |
| 391 |
|
| 392 |
if ( ! empty( self::$starter_kit['summary'] ) ) { |
| 393 |
echo '<p>' . \esc_html( self::$starter_kit['summary'] ) . '</p>'; |
| 394 |
} |
| 395 |
|
| 396 |
if ( ! empty( self::$starter_kit['attributedTo'] ) ) { |
| 397 |
echo \wp_kses_post( |
| 398 |
\sprintf( |
| 399 |
'Created by <a href="%1$s" target="_blank">%1$s</a>', |
| 400 |
\esc_url( self::$starter_kit['attributedTo'] ) |
| 401 |
) |
| 402 |
); |
| 403 |
} |
| 404 |
} |
| 405 |
|
| 406 |
/** |
| 407 |
* Render author selection. |
| 408 |
*/ |
| 409 |
private static function render_author_selection() { |
| 410 |
?> |
| 411 |
<h4><?php \esc_html_e( 'Select the author for the imported Starter Kit', 'activitypub' ); ?></h4> |
| 412 |
<p> |
| 413 |
<label for="author"><?php \esc_html_e( 'Author:', 'activitypub' ); ?></label> |
| 414 |
<?php |
| 415 |
\wp_dropdown_users( |
| 416 |
array( |
| 417 |
'name' => 'author', |
| 418 |
'id' => 'author', |
| 419 |
'show' => 'display_name_with_login', |
| 420 |
'selected' => \get_current_user_id(), |
| 421 |
'capability' => 'activitypub', |
| 422 |
) |
| 423 |
); |
| 424 |
?> |
| 425 |
</p> |
| 426 |
<?php |
| 427 |
} |
| 428 |
|
| 429 |
/** |
| 430 |
* Render actor selection. |
| 431 |
* |
| 432 |
* @param array $actors The actors to render. |
| 433 |
*/ |
| 434 |
private static function render_actor_selection( $actors ) { |
| 435 |
?> |
| 436 |
<h4><?php \esc_html_e( 'Select the accounts you want to follow', 'activitypub' ); ?></h4> |
| 437 |
<ul> |
| 438 |
<?php foreach ( $actors as $actor ) : ?> |
| 439 |
<?php |
| 440 |
$actor_uri = object_to_uri( $actor ); |
| 441 |
$actor_uri = \ltrim( $actor_uri, '@' ); |
| 442 |
|
| 443 |
if ( ! self::is_valid_actor( $actor_uri ) ) { |
| 444 |
continue; |
| 445 |
} |
| 446 |
|
| 447 |
$actor_name = \is_array( $actor ) ? ( $actor['name'] ?? '' ) : ''; |
| 448 |
?> |
| 449 |
<li> |
| 450 |
<label> |
| 451 |
<input type="checkbox" name="actors[]" value="<?php echo \esc_attr( $actor_uri ); ?>" checked /> |
| 452 |
<?php if ( $actor_name ) : ?> |
| 453 |
<strong><?php echo \esc_html( $actor_name ); ?></strong> |
| 454 |
(<?php echo \esc_html( $actor_uri ); ?>) |
| 455 |
<?php else : ?> |
| 456 |
<?php echo \esc_html( $actor_uri ); ?> |
| 457 |
<?php endif; ?> |
| 458 |
</label> |
| 459 |
</li> |
| 460 |
<?php endforeach; ?> |
| 461 |
</ul> |
| 462 |
<?php |
| 463 |
} |
| 464 |
|
| 465 |
/** |
| 466 |
* Check if actor URI is valid. |
| 467 |
* |
| 468 |
* @param string $actor_uri The actor URI to validate. |
| 469 |
* |
| 470 |
* @return bool True if the actor URI is valid, false otherwise. |
| 471 |
*/ |
| 472 |
private static function is_valid_actor( $actor_uri ) { |
| 473 |
return false !== \filter_var( $actor_uri, FILTER_VALIDATE_URL ) || false !== \filter_var( $actor_uri, FILTER_VALIDATE_EMAIL ); |
| 474 |
} |
| 475 |
|
| 476 |
/** |
| 477 |
* Import. |
| 478 |
*/ |
| 479 |
public static function import() { |
| 480 |
$error_message = \__( 'Sorry, there has been an error.', 'activitypub' ); |
| 481 |
|
| 482 |
\wp_suspend_cache_invalidation(); |
| 483 |
|
| 484 |
/** |
| 485 |
* Fires when the Starter Kit import starts. |
| 486 |
*/ |
| 487 |
\do_action( 'import_start' ); |
| 488 |
|
| 489 |
$result = self::follow(); |
| 490 |
|
| 491 |
\wp_suspend_cache_invalidation( false ); |
| 492 |
|
| 493 |
\wp_import_cleanup( self::$import_id ); |
| 494 |
|
| 495 |
if ( \is_wp_error( $result ) ) { |
| 496 |
\printf( '<p><strong>%s</strong><br />%s</p>', \esc_html( $error_message ), \esc_html( $result->get_error_message() ) ); |
| 497 |
} else { |
| 498 |
\printf( '<p>%s</p>', \esc_html__( 'All done.', 'activitypub' ) ); |
| 499 |
} |
| 500 |
|
| 501 |
/** |
| 502 |
* Fires when the Starter Kit import ends. |
| 503 |
*/ |
| 504 |
\do_action( 'import_end' ); |
| 505 |
} |
| 506 |
|
| 507 |
/** |
| 508 |
* Process posts. |
| 509 |
* |
| 510 |
* @return true|\WP_Error True on success, WP_Error on failure. |
| 511 |
*/ |
| 512 |
public static function follow() { |
| 513 |
$skipped = 0; |
| 514 |
$followed = 0; |
| 515 |
|
| 516 |
$items = self::$actor_list; |
| 517 |
|
| 518 |
foreach ( $items as $actor_id ) { |
| 519 |
$actor_id = object_to_uri( $actor_id ); |
| 520 |
$actor_id = \ltrim( $actor_id, '@' ); |
| 521 |
|
| 522 |
if ( ! \filter_var( $actor_id, FILTER_VALIDATE_URL ) && ! \filter_var( $actor_id, FILTER_VALIDATE_EMAIL ) ) { |
| 523 |
++$skipped; |
| 524 |
continue; |
| 525 |
} |
| 526 |
|
| 527 |
$result = follow( $actor_id, self::$author ); |
| 528 |
|
| 529 |
if ( \is_wp_error( $result ) ) { |
| 530 |
/* translators: %s: Account ID */ |
| 531 |
\printf( '<p>' . \esc_html__( '✗ %s', 'activitypub' ) . '</p>', \esc_html( $actor_id ) ); |
| 532 |
++$skipped; |
| 533 |
} else { |
| 534 |
/* translators: %s: Account ID */ |
| 535 |
\printf( '<p>' . \esc_html__( '✓ %s', 'activitypub' ) . '</p>', \esc_html( $actor_id ) ); |
| 536 |
++$followed; |
| 537 |
} |
| 538 |
} |
| 539 |
|
| 540 |
echo '<hr />'; |
| 541 |
|
| 542 |
/* translators: %d: Number of followed actors */ |
| 543 |
\printf( '<p>%s</p>', \esc_html( \sprintf( \_n( 'Followed %s Actor.', 'Followed %s Actors.', $followed, 'activitypub' ), \number_format_i18n( $followed ) ) ) ); |
| 544 |
/* translators: %d: Number of skipped items */ |
| 545 |
\printf( '<p>%s</p>', \esc_html( \sprintf( \_n( 'Skipped %s Item.', 'Skipped %s Items.', $skipped, 'activitypub' ), \number_format_i18n( $skipped ) ) ) ); |
| 546 |
|
| 547 |
return true; |
| 548 |
} |
| 549 |
|
| 550 |
/** |
| 551 |
* Intro. |
| 552 |
*/ |
| 553 |
public static function greet() { |
| 554 |
echo '<div class="narrow">'; |
| 555 |
echo '<p>' . \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' ) . '</p>'; |
| 556 |
|
| 557 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 558 |
$url = isset( $_GET['url'] ) ? \sanitize_text_field( \wp_unslash( $_GET['url'] ) ) : ''; |
| 559 |
|
| 560 |
if ( empty( $url ) ) { |
| 561 |
// File upload option. |
| 562 |
\printf( '<h3>%s</h3>', \esc_html__( 'Option 1: Upload a File', 'activitypub' ) ); |
| 563 |
\wp_import_upload_form( 'admin.php?import=starter-kit&step=1' ); |
| 564 |
|
| 565 |
// URL import option. |
| 566 |
\printf( '<h3>%s</h3>', \esc_html__( 'Option 2: Import from URL', 'activitypub' ) ); |
| 567 |
} else { |
| 568 |
// URL import option. |
| 569 |
\printf( '<h3>%s</h3>', \esc_html__( 'Import from URL', 'activitypub' ) ); |
| 570 |
} |
| 571 |
?> |
| 572 |
<form id="import-url-form" method="post" action="<?php echo \esc_url( \admin_url( 'admin.php?import=starter-kit&step=2' ) ); ?>"> |
| 573 |
<?php |
| 574 |
\wp_nonce_field( 'import-url' ); |
| 575 |
?> |
| 576 |
<p> |
| 577 |
<label for="import_url"><?php \esc_html_e( 'Starter Kit URL:', 'activitypub' ); ?><br /> |
| 578 |
<input type="url" id="import_url" name="import_url" size="50" class="code" placeholder="https://example.com/starter-kit.json" value="<?php echo \esc_attr( $url ); ?>" required /> |
| 579 |
</label> |
| 580 |
</p> |
| 581 |
<p class="submit"> |
| 582 |
<input type="submit" name="submit" id="submit" class="button" value="<?php \esc_attr_e( 'Import from URL', 'activitypub' ); ?>" /> |
| 583 |
</p> |
| 584 |
</form> |
| 585 |
|
| 586 |
</div> |
| 587 |
<?php |
| 588 |
} |
| 589 |
|
| 590 |
/** |
| 591 |
* Header. |
| 592 |
*/ |
| 593 |
public static function header() { |
| 594 |
echo '<div class="wrap">'; |
| 595 |
echo '<h2>' . \esc_html__( 'Import a Fediverse Starter Kit (Beta)', 'activitypub' ) . '</h2>'; |
| 596 |
} |
| 597 |
|
| 598 |
/** |
| 599 |
* Footer. |
| 600 |
*/ |
| 601 |
public static function footer() { |
| 602 |
echo '</div>'; |
| 603 |
} |
| 604 |
|
| 605 |
/** |
| 606 |
* Get actor list. |
| 607 |
*/ |
| 608 |
private static function get_actor_list() { |
| 609 |
$file = \get_attached_file( self::$import_id ); |
| 610 |
|
| 611 |
\WP_Filesystem(); |
| 612 |
|
| 613 |
global $wp_filesystem; |
| 614 |
|
| 615 |
$file_contents = $wp_filesystem->get_contents( $file ); |
| 616 |
if ( false === $file_contents ) { |
| 617 |
return new \WP_Error( 'file_not_found', \esc_html__( 'Could not read the uploaded file.', 'activitypub' ) ); |
| 618 |
} |
| 619 |
|
| 620 |
self::$starter_kit = \json_decode( $file_contents, true ); |
| 621 |
if ( null === self::$starter_kit ) { |
| 622 |
return new \WP_Error( 'invalid_json', \esc_html__( 'Invalid JSON format in the uploaded file.', 'activitypub' ) ); |
| 623 |
} |
| 624 |
|
| 625 |
/* |
| 626 |
* Validate that the type is a Collection-like type. |
| 627 |
* FeaturedCollection is from the Mastodon FEP draft: |
| 628 |
* https://github.com/mastodon/featured_collections/pull/1 |
| 629 |
*/ |
| 630 |
$type = (array) ( self::$starter_kit['type'] ?? array() ); |
| 631 |
$valid_types = array( 'Collection', 'OrderedCollection', 'FeaturedCollection' ); |
| 632 |
|
| 633 |
if ( ! \array_intersect( $type, $valid_types ) ) { |
| 634 |
return new \WP_Error( 'invalid_type', \esc_html__( 'The file does not contain a valid Starter Kit Collection.', 'activitypub' ) ); |
| 635 |
} |
| 636 |
|
| 637 |
$actors = self::$starter_kit['items'] ?? self::$starter_kit['orderedItems'] ?? array(); |
| 638 |
|
| 639 |
// Limit list to 150 actors. |
| 640 |
// TODO: Make this configurable. |
| 641 |
$actors = \array_slice( $actors, 0, 150 ); |
| 642 |
|
| 643 |
if ( ! $actors ) { |
| 644 |
return new \WP_Error( 'empty_actor_list', \esc_html__( 'The uploaded file does not contain any actors.', 'activitypub' ) ); |
| 645 |
} |
| 646 |
|
| 647 |
return $actors; |
| 648 |
} |
| 649 |
} |
| 650 |
|