PluginProbe
ActivityPub / 7.8.0
ActivityPub v7.8.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / wp-admin / import / class-starter-kit.php

class-starter-kit.php in ActivityPub 7.8.0, at includes/wp-admin/import/class-starter-kit.php

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