PluginProbe
ActivityPub / 8.3.0
ActivityPub v8.3.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 / class-moderation.php

class-moderation.php in ActivityPub 8.3.0, at includes/class-moderation.php

427 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Moderation class file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub;
9
10 use Activitypub\Activity\Activity;
11 use Activitypub\Activity\Actor;
12 use Activitypub\Collection\Actors;
13 use Activitypub\Collection\Blocked_Actors;
14
15 /**
16 * ActivityPub Moderation class.
17 *
18 * Handles user-specific blocking and site-wide moderation.
19 */
20 class Moderation {
21
22 /**
23 * Block type constants.
24 */
25 const TYPE_ACTOR = 'actor';
26 const TYPE_DOMAIN = 'domain';
27 const TYPE_KEYWORD = 'keyword';
28
29 /**
30 * Post meta key for blocked actors.
31 */
32 const BLOCKED_ACTORS_META_KEY = '_activitypub_blocked_by';
33
34 /**
35 * User meta key for blocked keywords.
36 */
37 const USER_META_KEYS = array(
38 self::TYPE_DOMAIN => 'activitypub_blocked_domains',
39 self::TYPE_KEYWORD => 'activitypub_blocked_keywords',
40 );
41
42 /**
43 * Option key for site-wide blocked keywords.
44 */
45 const OPTION_KEYS = array(
46 self::TYPE_DOMAIN => 'activitypub_site_blocked_domains',
47 self::TYPE_KEYWORD => 'activitypub_site_blocked_keywords',
48 );
49
50 /**
51 * Check if an activity should be blocked for a specific user.
52 *
53 * @param Activity $activity The activity.
54 * @param int|null $user_id The user ID to check blocks for.
55 * @return bool True if blocked, false otherwise.
56 */
57 public static function activity_is_blocked( $activity, $user_id = null ) {
58 if ( ! $activity instanceof Activity ) {
59 return false;
60 }
61
62 // First check site-wide blocks (admin moderation).
63 if ( self::activity_is_blocked_site_wide( $activity ) ) {
64 return true;
65 }
66
67 // Then check user-specific blocks.
68 if ( $user_id && self::activity_is_blocked_for_user( $activity, $user_id ) ) {
69 return true;
70 }
71
72 $remote_addr = \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
73 $user_agent = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
74
75 // Fall back to WordPress comment disallowed list.
76 return \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', $activity->get_content(), $remote_addr, $user_agent );
77 }
78
79 /**
80 * Check if an activity is blocked site-wide.
81 *
82 * @param Activity $activity The activity.
83 * @return bool True if blocked, false otherwise.
84 */
85 public static function activity_is_blocked_site_wide( $activity ) {
86 $blocks = self::get_site_blocks();
87
88 return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] );
89 }
90
91 /**
92 * Check if an activity is blocked for a specific user.
93 *
94 * @param Activity $activity The activity.
95 * @param int $user_id The user ID.
96 * @return bool True if blocked, false otherwise.
97 */
98 public static function activity_is_blocked_for_user( $activity, $user_id ) {
99 $blocks = self::get_user_blocks( $user_id );
100
101 return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] );
102 }
103
104 /**
105 * Add a block for a user.
106 *
107 * @param int $user_id The user ID.
108 * @param string $type The block type (actor, domain, keyword).
109 * @param string $value The value to block.
110 * @return bool True on success, false on failure.
111 */
112 public static function add_user_block( $user_id, $type, $value ) {
113 switch ( $type ) {
114 case self::TYPE_ACTOR:
115 return Blocked_Actors::add( $user_id, $value );
116
117 case self::TYPE_DOMAIN:
118 case self::TYPE_KEYWORD:
119 $blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array();
120
121 if ( ! \in_array( $value, $blocks, true ) ) {
122 /**
123 * Fired when a domain or keyword is blocked.
124 *
125 * @param string $value The blocked domain or keyword.
126 * @param string $type The block type (actor, domain, keyword).
127 * @param int $user_id The user ID.
128 */
129 \do_action( 'activitypub_add_user_block', $value, $type, $user_id );
130
131 $blocks[] = $value;
132 return (bool) \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], $blocks );
133 }
134 break;
135 }
136
137 return true; // Already blocked.
138 }
139
140 /**
141 * Remove a block for a user.
142 *
143 * @param int $user_id The user ID.
144 * @param string $type The block type (actor, domain, keyword).
145 * @param string $value The value to unblock.
146 * @return bool True on success, false on failure.
147 */
148 public static function remove_user_block( $user_id, $type, $value ) {
149 switch ( $type ) {
150 case self::TYPE_ACTOR:
151 return Blocked_Actors::remove( $user_id, $value );
152
153 case self::TYPE_DOMAIN:
154 case self::TYPE_KEYWORD:
155 $blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array();
156 $key = \array_search( $value, $blocks, true );
157
158 if ( false !== $key ) {
159 /**
160 * Fired when a domain or keyword is unblocked.
161 *
162 * @param string $value The unblocked domain or keyword.
163 * @param string $type The block type (actor, domain, keyword).
164 * @param int $user_id The user ID.
165 */
166 \do_action( 'activitypub_remove_user_block', $value, $type, $user_id );
167
168 unset( $blocks[ $key ] );
169 return \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], \array_values( $blocks ) );
170 }
171 break;
172 }
173
174 return true; // Not blocked anyway.
175 }
176
177 /**
178 * Get all blocks for a user.
179 *
180 * @param int $user_id The user ID.
181 * @return array Array of blocks organized by type.
182 */
183 public static function get_user_blocks( $user_id ) {
184 return array(
185 'actors' => \wp_list_pluck( Blocked_Actors::get_many( $user_id ), 'guid' ),
186 'domains' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_DOMAIN ], true ) ?: array(),
187 'keywords' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_KEYWORD ], true ) ?: array(),
188 );
189 }
190
191 /**
192 * Add a site-wide block.
193 *
194 * @param string $type The block type (actor, domain, keyword).
195 * @param string $value The value to block.
196 * @return bool True on success, false on failure.
197 */
198 public static function add_site_block( $type, $value ) {
199 switch ( $type ) {
200 case self::TYPE_ACTOR:
201 // Site-wide actor blocking uses the BLOG_USER_ID.
202 return self::add_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value );
203
204 case self::TYPE_DOMAIN:
205 case self::TYPE_KEYWORD:
206 $blocks = \get_option( self::OPTION_KEYS[ $type ], array() );
207
208 if ( ! \in_array( $value, $blocks, true ) ) {
209 /**
210 * Fired when a domain or keyword is blocked site-wide.
211 *
212 * @param string $value The blocked domain or keyword.
213 * @param string $type The block type (actor, domain, keyword).
214 */
215 \do_action( 'activitypub_add_site_block', $value, $type );
216
217 $blocks[] = $value;
218 return \update_option( self::OPTION_KEYS[ $type ], $blocks );
219 }
220 break;
221 }
222
223 return true; // Already blocked.
224 }
225
226 /**
227 * Add multiple site-wide blocks at once.
228 *
229 * More efficient than calling add_site_block() in a loop as it
230 * performs a single database update.
231 *
232 * @param string $type The block type (domain or keyword only).
233 * @param array $values Array of values to block.
234 */
235 public static function add_site_blocks( $type, $values ) {
236 if ( ! in_array( $type, array( self::TYPE_DOMAIN, self::TYPE_KEYWORD ), true ) ) {
237 return;
238 }
239
240 if ( empty( $values ) ) {
241 return;
242 }
243
244 foreach ( $values as $value ) {
245 /**
246 * Fired when a domain or keyword is blocked site-wide.
247 *
248 * @param string $value The blocked domain or keyword.
249 * @param string $type The block type (actor, domain, keyword).
250 */
251 \do_action( 'activitypub_add_site_block', $value, $type );
252 }
253
254 $existing = \get_option( self::OPTION_KEYS[ $type ], array() );
255 \update_option( self::OPTION_KEYS[ $type ], array_unique( array_merge( $existing, $values ) ) );
256 }
257
258 /**
259 * Remove a site-wide block.
260 *
261 * @param string $type The block type (actor, domain, keyword).
262 * @param string $value The value to unblock.
263 * @return bool True on success, false on failure.
264 */
265 public static function remove_site_block( $type, $value ) {
266 switch ( $type ) {
267 case self::TYPE_ACTOR:
268 // Site-wide actor unblocking uses the BLOG_USER_ID.
269 return self::remove_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value );
270
271 case self::TYPE_DOMAIN:
272 case self::TYPE_KEYWORD:
273 $blocks = \get_option( self::OPTION_KEYS[ $type ], array() );
274 $key = \array_search( $value, $blocks, true );
275
276 if ( false !== $key ) {
277 /**
278 * Fired when a domain or keyword is unblocked site-wide.
279 *
280 * @param string $value The unblocked domain or keyword.
281 * @param string $type The block type (actor, domain, keyword).
282 */
283 \do_action( 'activitypub_remove_site_block', $value, $type );
284
285 unset( $blocks[ $key ] );
286 return \update_option( self::OPTION_KEYS[ $type ], \array_values( $blocks ) );
287 }
288 break;
289 }
290
291 return true; // Not blocked anyway.
292 }
293
294 /**
295 * Get all site-wide blocks.
296 *
297 * @return array Array of blocks organized by type.
298 */
299 public static function get_site_blocks() {
300 return array(
301 'actors' => \wp_list_pluck( Blocked_Actors::get_many( Actors::BLOG_USER_ID ), 'guid' ),
302 'domains' => \get_option( self::OPTION_KEYS[ self::TYPE_DOMAIN ], array() ),
303 'keywords' => \get_option( self::OPTION_KEYS[ self::TYPE_KEYWORD ], array() ),
304 );
305 }
306
307 /**
308 * Check if an actor is blocked by user or site-wide.
309 *
310 * @param string $actor_uri Actor URI to check.
311 * @param int $user_id Optional. User ID to check user blocks for. Defaults to 0 (site-wide only).
312 * @return bool True if blocked, false otherwise.
313 */
314 public static function is_actor_blocked( $actor_uri, $user_id = 0 ) {
315 if ( ! $actor_uri ) {
316 return false;
317 }
318
319 // Check site-wide blocks.
320 $site_blocks = self::get_site_blocks();
321 if ( \in_array( $actor_uri, $site_blocks['actors'], true ) ) {
322 return true;
323 }
324
325 // Check site-wide domain blocks.
326 $actor_domain = \wp_parse_url( $actor_uri, PHP_URL_HOST );
327 if ( $actor_domain && \in_array( $actor_domain, $site_blocks['domains'], true ) ) {
328 return true;
329 }
330
331 // Check user-specific blocks if user_id is provided.
332 if ( $user_id > 0 ) {
333 $user_blocks = self::get_user_blocks( $user_id );
334 if ( \in_array( $actor_uri, $user_blocks['actors'], true ) ) {
335 return true;
336 }
337
338 // Check user-specific domain blocks.
339 if ( $actor_domain && \in_array( $actor_domain, $user_blocks['domains'], true ) ) {
340 return true;
341 }
342 }
343
344 return false;
345 }
346
347 /**
348 * Check activity against blocklists.
349 *
350 * @param Activity $activity The activity.
351 * @param array $blocked_actors List of blocked actors.
352 * @param array $blocked_domains List of blocked domains.
353 * @param array $blocked_keywords List of blocked keywords.
354 * @return bool True if blocked, false otherwise.
355 */
356 private static function check_activity_against_blocks( $activity, $blocked_actors, $blocked_domains, $blocked_keywords ) {
357 $has_object = \is_object( $activity->get_object() );
358
359 // Extract actor information.
360 $actor_id = object_to_uri( $activity->get_actor() );
361
362 // Check blocked actors.
363 if ( $actor_id ) {
364 // If actor_id is not a URL, resolve it via webfinger.
365 if ( ! \str_starts_with( $actor_id, 'http' ) ) {
366 $resolved_url = Webfinger::resolve( $actor_id );
367 if ( ! \is_wp_error( $resolved_url ) ) {
368 $actor_id = $resolved_url;
369 }
370 }
371
372 if ( \in_array( $actor_id, $blocked_actors, true ) ) {
373 return true;
374 }
375 }
376
377 // Check blocked domains.
378 $urls = array(
379 \wp_parse_url( $actor_id, PHP_URL_HOST ),
380 \wp_parse_url( $activity->get_id(), PHP_URL_HOST ),
381 \wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ),
382 );
383 foreach ( $blocked_domains as $domain ) {
384 if ( \in_array( $domain, $urls, true ) ) {
385 return true;
386 }
387 }
388
389 // Check blocked keywords in activity content.
390 if ( $has_object ) {
391 $object = $activity->get_object();
392 $content_map = array();
393 $content_map[] = $object->get_content();
394 $content_map[] = $object->get_summary();
395 $content_map[] = $object->get_name();
396
397 if ( is_actor( $object ) ) {
398 /* @var Actor $object Actor object */
399 $content_map[] = $object->get_preferred_username();
400 }
401
402 if ( \is_array( $object->get_content_map() ) ) {
403 $content_map = \array_merge( $content_map, \array_values( $object->get_content_map() ) );
404 }
405
406 if ( \is_array( $object->get_summary_map() ) ) {
407 $content_map = \array_merge( $content_map, \array_values( $object->get_summary_map() ) );
408 }
409
410 if ( \is_array( $object->get_name_map() ) ) {
411 $content_map = \array_merge( $content_map, \array_values( $object->get_name_map() ) );
412 }
413
414 $content_map = \array_filter( $content_map );
415 $content = \implode( ' ', $content_map );
416
417 foreach ( $blocked_keywords as $keyword ) {
418 if ( \stripos( $content, $keyword ) !== false ) {
419 return true;
420 }
421 }
422 }
423
424 return false;
425 }
426 }
427