PluginProbe
ActivityPub / 7.7.0
ActivityPub v7.7.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 7.7.0, at includes/class-moderation.php

395 lines 11.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 * Remove a site-wide block.
228 *
229 * @param string $type The block type (actor, domain, keyword).
230 * @param string $value The value to unblock.
231 * @return bool True on success, false on failure.
232 */
233 public static function remove_site_block( $type, $value ) {
234 switch ( $type ) {
235 case self::TYPE_ACTOR:
236 // Site-wide actor unblocking uses the BLOG_USER_ID.
237 return self::remove_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value );
238
239 case self::TYPE_DOMAIN:
240 case self::TYPE_KEYWORD:
241 $blocks = \get_option( self::OPTION_KEYS[ $type ], array() );
242 $key = \array_search( $value, $blocks, true );
243
244 if ( false !== $key ) {
245 /**
246 * Fired when a domain or keyword is unblocked site-wide.
247 *
248 * @param string $value The unblocked domain or keyword.
249 * @param string $type The block type (actor, domain, keyword).
250 */
251 \do_action( 'activitypub_remove_site_block', $value, $type );
252
253 unset( $blocks[ $key ] );
254 return \update_option( self::OPTION_KEYS[ $type ], \array_values( $blocks ) );
255 }
256 break;
257 }
258
259 return true; // Not blocked anyway.
260 }
261
262 /**
263 * Get all site-wide blocks.
264 *
265 * @return array Array of blocks organized by type.
266 */
267 public static function get_site_blocks() {
268 return array(
269 'actors' => \wp_list_pluck( Blocked_Actors::get_many( Actors::BLOG_USER_ID ), 'guid' ),
270 'domains' => \get_option( self::OPTION_KEYS[ self::TYPE_DOMAIN ], array() ),
271 'keywords' => \get_option( self::OPTION_KEYS[ self::TYPE_KEYWORD ], array() ),
272 );
273 }
274
275 /**
276 * Check if an actor is blocked by user or site-wide.
277 *
278 * @param string $actor_uri Actor URI to check.
279 * @param int $user_id Optional. User ID to check user blocks for. Defaults to 0 (site-wide only).
280 * @return bool True if blocked, false otherwise.
281 */
282 public static function is_actor_blocked( $actor_uri, $user_id = 0 ) {
283 if ( ! $actor_uri ) {
284 return false;
285 }
286
287 // Check site-wide blocks.
288 $site_blocks = self::get_site_blocks();
289 if ( \in_array( $actor_uri, $site_blocks['actors'], true ) ) {
290 return true;
291 }
292
293 // Check site-wide domain blocks.
294 $actor_domain = \wp_parse_url( $actor_uri, PHP_URL_HOST );
295 if ( $actor_domain && \in_array( $actor_domain, $site_blocks['domains'], true ) ) {
296 return true;
297 }
298
299 // Check user-specific blocks if user_id is provided.
300 if ( $user_id > 0 ) {
301 $user_blocks = self::get_user_blocks( $user_id );
302 if ( \in_array( $actor_uri, $user_blocks['actors'], true ) ) {
303 return true;
304 }
305
306 // Check user-specific domain blocks.
307 if ( $actor_domain && \in_array( $actor_domain, $user_blocks['domains'], true ) ) {
308 return true;
309 }
310 }
311
312 return false;
313 }
314
315 /**
316 * Check activity against blocklists.
317 *
318 * @param Activity $activity The activity.
319 * @param array $blocked_actors List of blocked actors.
320 * @param array $blocked_domains List of blocked domains.
321 * @param array $blocked_keywords List of blocked keywords.
322 * @return bool True if blocked, false otherwise.
323 */
324 private static function check_activity_against_blocks( $activity, $blocked_actors, $blocked_domains, $blocked_keywords ) {
325 $has_object = \is_object( $activity->get_object() );
326
327 // Extract actor information.
328 $actor_id = object_to_uri( $activity->get_actor() );
329
330 // Check blocked actors.
331 if ( $actor_id ) {
332 // If actor_id is not a URL, resolve it via webfinger.
333 if ( ! \str_starts_with( $actor_id, 'http' ) ) {
334 $resolved_url = Webfinger::resolve( $actor_id );
335 if ( ! \is_wp_error( $resolved_url ) ) {
336 $actor_id = $resolved_url;
337 }
338 }
339
340 if ( \in_array( $actor_id, $blocked_actors, true ) ) {
341 return true;
342 }
343 }
344
345 // Check blocked domains.
346 $urls = array(
347 \wp_parse_url( $actor_id, PHP_URL_HOST ),
348 \wp_parse_url( $activity->get_id(), PHP_URL_HOST ),
349 \wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ),
350 );
351 foreach ( $blocked_domains as $domain ) {
352 if ( \in_array( $domain, $urls, true ) ) {
353 return true;
354 }
355 }
356
357 // Check blocked keywords in activity content.
358 if ( $has_object ) {
359 $object = $activity->get_object();
360 $content_map = array();
361 $content_map[] = $object->get_content();
362 $content_map[] = $object->get_summary();
363 $content_map[] = $object->get_name();
364
365 if ( is_actor( $object ) ) {
366 /* @var Actor $object Actor object */
367 $content_map[] = $object->get_preferred_username();
368 }
369
370 if ( \is_array( $object->get_content_map() ) ) {
371 $content_map = \array_merge( $content_map, \array_values( $object->get_content_map() ) );
372 }
373
374 if ( \is_array( $object->get_summary_map() ) ) {
375 $content_map = \array_merge( $content_map, \array_values( $object->get_summary_map() ) );
376 }
377
378 if ( \is_array( $object->get_name_map() ) ) {
379 $content_map = \array_merge( $content_map, \array_values( $object->get_name_map() ) );
380 }
381
382 $content_map = \array_filter( $content_map );
383 $content = \implode( ' ', $content_map );
384
385 foreach ( $blocked_keywords as $keyword ) {
386 if ( \stripos( $content, $keyword ) !== false ) {
387 return true;
388 }
389 }
390 }
391
392 return false;
393 }
394 }
395