PluginProbe
ActivityPub / 9.2.0
ActivityPub v9.2.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 / table / class-following.php

class-following.php in ActivityPub 9.2.0, at includes/wp-admin/table/class-following.php

491 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Followers Table-Class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\WP_Admin\Table;
9
10 use Activitypub\Collection\Actors;
11 use Activitypub\Collection\Following as Following_Collection;
12 use Activitypub\Collection\Remote_Actors;
13 use Activitypub\Moderation;
14 use Activitypub\Sanitize;
15 use Activitypub\Webfinger;
16
17 use function Activitypub\follow;
18 use function Activitypub\object_to_uri;
19
20 if ( ! \class_exists( '\WP_List_Table' ) ) {
21 require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
22 }
23
24 /**
25 * Following Table-Class.
26 */
27 class Following extends \WP_List_Table {
28 use Actor_List_Table;
29
30 /**
31 * User ID.
32 *
33 * @var int
34 */
35 private $user_id;
36
37 /**
38 * Constructor.
39 */
40 public function __construct() {
41 if ( \get_current_screen()->id === 'settings_page_activitypub' ) {
42 $this->user_id = Actors::BLOG_USER_ID;
43 } else {
44 $this->user_id = \get_current_user_id();
45
46 \add_action( 'admin_notices', array( $this, 'process_admin_notices' ) );
47 }
48
49 parent::__construct(
50 array(
51 'singular' => \__( 'Following', 'activitypub' ),
52 'plural' => \__( 'Followings', 'activitypub' ),
53 'ajax' => false,
54 )
55 );
56
57 \add_action( 'load-' . \get_current_screen()->id, array( $this, 'process_action' ), 20 );
58 }
59
60 /**
61 * Process action.
62 */
63 public function process_action() {
64 if ( ! \current_user_can( 'edit_user', $this->user_id ) ) {
65 return;
66 }
67
68 if ( ! $this->current_action() ) {
69 return;
70 }
71
72 $redirect_to = \add_query_arg(
73 array(
74 'settings-updated' => true, // Tell WordPress to load settings errors transient.
75 'action' => false, // Remove action parameter to prevent redirect loop.
76 )
77 );
78
79 switch ( $this->current_action() ) {
80 case 'delete':
81 $redirect_to = \remove_query_arg( array( 'follower', 'following' ), $redirect_to );
82
83 // Handle single follower deletion.
84 if ( isset( $_GET['follower'], $_GET['_wpnonce'] ) ) {
85 $follower = \absint( $_GET['follower'] );
86 $nonce = \sanitize_text_field( \wp_unslash( $_GET['_wpnonce'] ) );
87
88 if ( \wp_verify_nonce( $nonce, 'delete-follower_' . $follower ) ) {
89 Following_Collection::unfollow( $follower, $this->user_id );
90
91 \add_settings_error( 'activitypub', 'follower_deleted', \__( 'Account unfollowed.', 'activitypub' ), 'success' );
92 }
93 }
94
95 // Handle bulk actions.
96 if ( isset( $_REQUEST['following'], $_REQUEST['_wpnonce'] ) ) {
97 $nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
98
99 if ( \wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
100 $following = \array_map( 'absint', \wp_unslash( $_REQUEST['following'] ) );
101
102 foreach ( $following as $post_id ) {
103 Following_Collection::unfollow( $post_id, $this->user_id );
104 }
105
106 $count = \count( $following );
107 /* translators: %d: Number of accounts unfollowed. */
108 $message = \_n( '%d account unfollowed.', '%d accounts unfollowed.', $count, 'activitypub' );
109 $message = \sprintf( $message, \number_format_i18n( $count ) );
110
111 \add_settings_error( 'activitypub', 'followers_deleted', $message, 'success' );
112 }
113 }
114 break;
115 case 'follow':
116 $redirect_to = \remove_query_arg( array( 'resource', 's' ), $redirect_to );
117
118 if ( ! isset( $_REQUEST['activitypub-profile'], $_REQUEST['_wpnonce'] ) ) {
119 return;
120 }
121
122 $nonce = \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) );
123 if ( ! \wp_verify_nonce( $nonce, 'activitypub-follow-nonce' ) ) {
124 return;
125 }
126
127 $original = \sanitize_text_field( \wp_unslash( $_REQUEST['activitypub-profile'] ) );
128 $profile = Remote_Actors::normalize_identifier( $original );
129 if ( ! $profile ) {
130 /* translators: %s: Account profile that could not be followed */
131 \add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account &#8220;%s&#8221;. Please verify the account exists and try again.', 'activitypub' ), \esc_html( $original ) ) );
132 $redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
133 break;
134 }
135
136 // Check if actor is blocked.
137 if ( Moderation::is_actor_blocked( $profile, $this->user_id ) ) {
138 /* translators: %s: Account profile that could not be followed */
139 \add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account &#8220;%s&#8221;. The account is blocked.', 'activitypub' ), \esc_html( $profile ) ) );
140 $redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
141 break;
142 }
143
144 // Check if already following before making the request.
145 $actor = Remote_Actors::get_by_uri( $profile );
146 if ( ! \is_wp_error( $actor ) ) {
147 $following = \get_post_meta( $actor->ID, Following_Collection::FOLLOWING_META_KEY, false );
148 $pending = \get_post_meta( $actor->ID, Following_Collection::PENDING_META_KEY, false );
149 if ( \in_array( (string) $this->user_id, $following, true ) ) {
150 /* translators: %s: Account profile that is already being followed */
151 \add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'You are already following &#8220;%s&#8221;.', 'activitypub' ), \esc_html( $profile ) ), 'info' );
152 break;
153 } elseif ( \in_array( (string) $this->user_id, $pending, true ) ) {
154 /* translators: %s: Account profile that has a pending follow request */
155 \add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'You already have a pending follow request for &#8220;%s&#8221;.', 'activitypub' ), \esc_html( $profile ) ), 'info' );
156 break;
157 }
158 }
159
160 $result = follow( $profile, $this->user_id );
161 if ( \is_wp_error( $result ) ) {
162 /* translators: %s: Account profile that could not be followed */
163 \add_settings_error( 'activitypub', 'followed', \sprintf( \__( 'Unable to follow account &#8220;%s&#8221;. Please verify the account exists and try again.', 'activitypub' ), \esc_html( $profile ) ) );
164 $redirect_to = \add_query_arg( 'resource', $original, $redirect_to );
165 } else {
166 \add_settings_error( 'activitypub', 'followed', \__( 'Account followed.', 'activitypub' ), 'success' );
167 }
168
169 break;
170 default:
171 break;
172 }
173
174 \set_transient( 'settings_errors', \get_settings_errors(), 30 ); // 30 seconds.
175
176 \wp_safe_redirect( $redirect_to );
177 exit;
178 }
179
180 /**
181 * Process admin notices based on query parameters.
182 */
183 public function process_admin_notices() {
184 \settings_errors( 'activitypub' );
185 }
186
187 /**
188 * Prepare items.
189 */
190 public function prepare_items() {
191 $status = Following_Collection::ALL;
192 $page_num = $this->get_pagenum();
193 $per_page = $this->get_items_per_page( 'activitypub_following_per_page' );
194 $args = array();
195
196 // phpcs:disable WordPress.Security.NonceVerification.Recommended
197 if ( isset( $_GET['orderby'] ) ) {
198 $args['orderby'] = \sanitize_text_field( \wp_unslash( $_GET['orderby'] ) );
199 }
200
201 if ( isset( $_GET['order'] ) ) {
202 $args['order'] = \sanitize_text_field( \wp_unslash( $_GET['order'] ) );
203 }
204
205 if ( isset( $_GET['s'] ) ) {
206 $args['s'] = $this->normalize_search_term( \wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
207 }
208
209 if ( isset( $_GET['status'] ) ) {
210 $status = \sanitize_text_field( \wp_unslash( $_GET['status'] ) );
211 }
212
213 if ( Following_Collection::PENDING === $status ) {
214 $following_with_count = Following_Collection::query_pending( $this->user_id, $per_page, $page_num, $args );
215 } elseif ( Following_Collection::ACCEPTED === $status ) {
216 $following_with_count = Following_Collection::query( $this->user_id, $per_page, $page_num, $args );
217 } else {
218 $following_with_count = Following_Collection::query_all( $this->user_id, $per_page, $page_num, $args );
219 }
220
221 $followings = $following_with_count['following'];
222 $counter = $following_with_count['total'];
223
224 $this->items = array();
225 $this->set_pagination_args(
226 array(
227 'total_items' => $counter,
228 'total_pages' => \ceil( $counter / $per_page ),
229 'per_page' => $per_page,
230 )
231 );
232
233 foreach ( $followings as $following ) {
234 $actor = Remote_Actors::get_actor( $following );
235 if ( \is_wp_error( $actor ) ) {
236 continue;
237 }
238
239 $this->items[] = array(
240 'id' => $following->ID,
241 'icon' => object_to_uri( $actor->get_icon() ?? ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg' ),
242 'post_title' => $actor->get_name() ?: $actor->get_preferred_username(),
243 'username' => $actor->get_preferred_username(),
244 'url' => object_to_uri( $actor->get_url() ?: $actor->get_id() ),
245 'webfinger' => $actor->get_webfinger(),
246 'status' => Following_Collection::check_status( $this->user_id, $following->ID ),
247 'identifier' => $actor->get_id(),
248 'modified' => $following->post_modified_gmt,
249 );
250 }
251 }
252
253 /**
254 * Returns views.
255 *
256 * @return string[]
257 */
258 public function get_views() {
259 $count = Following_Collection::count_by_status( $this->user_id );
260 $path = 'users.php?page=activitypub-following-list';
261 $status = Following_Collection::ALL;
262
263 if ( Actors::BLOG_USER_ID === $this->user_id ) {
264 $path = 'options-general.php?page=activitypub&tab=following';
265 }
266
267 if ( ! empty( $_GET['status'] ) ) {
268 $status = \sanitize_text_field( \wp_unslash( $_GET['status'] ) );
269 }
270
271 $links = array(
272 'all' => array(
273 'url' => \admin_url( $path ),
274 'label' => \sprintf(
275 /* translators: %s: Number of users. */
276 \_nx(
277 'All <span class="count">(%s)</span>',
278 'All <span class="count">(%s)</span>',
279 $count[ Following_Collection::ALL ],
280 'users',
281 'activitypub'
282 ),
283 \number_format_i18n( $count[ Following_Collection::ALL ] )
284 ),
285 'current' => Following_Collection::ALL === $status,
286 ),
287 'accepted' => array(
288 'url' => \admin_url( $path . '&status=' . Following_Collection::ACCEPTED ),
289 'label' => \sprintf(
290 /* translators: %s: Number of users. */
291 \_nx(
292 'Accepted <span class="count">(%s)</span>',
293 'Accepted <span class="count">(%s)</span>',
294 $count[ Following_Collection::ACCEPTED ],
295 'users',
296 'activitypub'
297 ),
298 \number_format_i18n( $count[ Following_Collection::ACCEPTED ] )
299 ),
300 'current' => Following_Collection::ACCEPTED === $status,
301 ),
302 'pending' => array(
303 'url' => \admin_url( $path . '&status=' . Following_Collection::PENDING ),
304 'label' => \sprintf(
305 /* translators: %s: Number of users. */
306 \_nx(
307 'Pending <span class="count">(%s)</span>',
308 'Pending <span class="count">(%s)</span>',
309 $count[ Following_Collection::PENDING ],
310 'users',
311 'activitypub'
312 ),
313 \number_format_i18n( $count[ Following_Collection::PENDING ] )
314 ),
315 'current' => Following_Collection::PENDING === $status,
316 ),
317 );
318
319 return $this->get_views_links( $links );
320 }
321
322 /**
323 * Returns bulk actions.
324 *
325 * @return array
326 */
327 public function get_bulk_actions() {
328 return array(
329 'delete' => \__( 'Unfollow', 'activitypub' ),
330 );
331 }
332
333 /**
334 * Column avatar.
335 *
336 * @param array $item Item.
337 * @return string
338 */
339 public function column_cb( $item ) {
340 return \sprintf( '<input type="checkbox" name="following[]" value="%s" />', \esc_attr( $item['id'] ) );
341 }
342
343 /**
344 * Column url.
345 *
346 * @param array $item Item.
347 * @return string
348 */
349 public function column_username( $item ) {
350 $status = '';
351
352 if (
353 ( ! isset( $_GET['status'] ) || Following_Collection::ALL === $_GET['status'] ) &&
354 ( Following_Collection::PENDING === $item['status'] )
355 ) {
356 $status = \sprintf( '<strong class="pending"> — %s</strong>', \esc_html__( 'Pending', 'activitypub' ) );
357 }
358
359 return \sprintf(
360 '<img src="%1$s" width="32" height="32" alt="%2$s" loading="lazy"/> <strong><a href="%3$s" target="_blank">%4$s</a></strong>%5$s<br />',
361 \esc_url( $item['icon'] ),
362 \esc_attr( $item['post_title'] ),
363 \esc_url( $item['url'] ),
364 \esc_html( $item['username'] ),
365 $status
366 );
367 }
368
369 /**
370 * Column WebFinger.
371 *
372 * @param array $item Item.
373 *
374 * @return string The WebFinger link.
375 */
376 public function column_webfinger( $item ) {
377 $webfinger = Sanitize::webfinger( $item['webfinger'] );
378
379 return \sprintf(
380 '<a href="%1$s" target="_blank" title="%1$s">@%2$s</a>',
381 \esc_url( $item['url'] ),
382 \esc_html( $webfinger )
383 );
384 }
385
386 /**
387 * Column modified.
388 *
389 * @param array $item Item.
390 * @return string
391 */
392 public function column_modified( $item ) {
393 $modified = \strtotime( $item['modified'] );
394 return \sprintf(
395 '<time datetime="%1$s">%2$s</time>',
396 \esc_attr( \gmdate( 'c', $modified ) ),
397 \esc_html( \gmdate( \get_option( 'date_format' ), $modified ) )
398 );
399 }
400
401 /**
402 * Message to be displayed when there are no followings.
403 */
404 public function no_items() {
405 \esc_html_e( 'No profiles found.', 'activitypub' );
406
407 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
408 $search = \sanitize_text_field( \wp_unslash( $_GET['s'] ?? '' ) );
409 if ( empty( $search ) ) {
410 return;
411 }
412
413 $search = Sanitize::webfinger( $search );
414 if ( \filter_var( $search, FILTER_VALIDATE_EMAIL ) ) {
415 return;
416 }
417
418 $search = Webfinger::resolve( $search );
419
420 if ( ! \is_wp_error( $search ) && \filter_var( $search, FILTER_VALIDATE_URL ) ) {
421 $actor = Remote_Actors::fetch_by_uri( $search );
422 if ( ! \is_wp_error( $actor ) ) {
423 echo ' ';
424 \printf(
425 /* translators: %s: Actor name. */
426 \esc_html__( 'Would you like to follow %s?', 'activitypub' ),
427 \sprintf(
428 '<a href="%s">%s</a>',
429 \esc_url( \add_query_arg( 'resource', $search ) ),
430 \esc_html( $actor->post_title )
431 )
432 );
433 }
434 }
435 }
436
437 /**
438 * Single row.
439 *
440 * @param array $item Item.
441 */
442 public function single_row( $item ) {
443 \printf(
444 '<tr id="following-%1$s" class="status-%2$s">',
445 \esc_attr( $item['id'] ),
446 \esc_attr( $item['status'] )
447 );
448 $this->single_row_columns( $item );
449 \printf( "</tr>\n" );
450 }
451
452 /**
453 * Handles the row actions for each following item.
454 *
455 * @param array $item The current following item.
456 * @param string $column_name The current column name.
457 * @param string $primary The primary column name.
458 *
459 * @return string HTML for the row actions.
460 */
461 protected function handle_row_actions( $item, $column_name, $primary ) {
462 if ( $column_name !== $primary ) {
463 return '';
464 }
465
466 $actions = array(
467 'unfollow' => \sprintf(
468 '<a href="%s" aria-label="%s">%s</a>',
469 $this->get_action_url( 'delete', $item['id'] ),
470 /* translators: %s: username. */
471 \esc_attr( \sprintf( \__( 'Unfollow %s', 'activitypub' ), $item['username'] ) ),
472 \esc_html__( 'Unfollow', 'activitypub' )
473 ),
474 );
475
476 /**
477 * Filters the array of row action links on the Following list table.
478 *
479 * This filter allows you to modify the row actions for each following item in the Following list table.
480 *
481 * @since 7.5.0
482 *
483 * @param string[] $actions An array of row action links. Defaults include 'Unfollow'.
484 * @param array $item The current following item.
485 */
486 $actions = \apply_filters( 'activitypub_following_row_actions', $actions, $item );
487
488 return $this->row_actions( $actions );
489 }
490 }
491