PluginProbe
wpForo Forum / 3.1.4
wpForo Forum v3.1.4
3.1.5 3.1.4 3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.4.0 1.4.1 1.4.10 1.4.11 1.4.12 1.4.13 1.4.2 All 137 releases
wpforo / classes / Moderation.php

Moderation.php in wpForo Forum 3.1.4, at classes/Moderation.php

755 lines 27.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace wpforo\classes;
4
5 use Akismet;
6 use wpforo\admin\listtables\Moderations as ModerationsListTable;
7
8 // Exit if accessed directly
9 if( ! defined( 'ABSPATH' ) ) exit;
10
11 class Moderation {
12 public $post_statuses;
13 public $list_table;
14
15 public function __construct() {
16 $this->post_statuses = apply_filters( 'wpforo_post_statuses', [ 0 => 'approved', 1 => 'unapproved' ] );
17 add_action( 'wpforo_after_change_board', function() {
18 if( ! is_null( WPF()->wp_current_user ) ) $this->init();
19 } );
20 add_action( 'wpforo_after_post_report', function( $postid ) {
21 $this->after_post_report( $postid );
22 } );
23 }
24
25 private function init() {
26 if( is_admin() ) add_action( 'wpforo_after_init', [ $this, 'init_list_table' ] );
27 if ( wpforo_setting( 'antispam', 'spam_filter' ) ){
28 add_filter( 'wpforo_add_topic_data_filter', [ &$this, 'auto_moderate' ], 8 );
29 add_filter( 'wpforo_add_post_data_filter', [ &$this, 'auto_moderate' ], 8 );
30 if( WPF()->member->current_user_is_new() ){
31 if( class_exists( 'Akismet' ) ) {
32 add_filter( 'wpforo_add_topic_data_filter', [ &$this, 'akismet_topic' ], 9 );
33 add_filter( 'wpforo_edit_topic_data_filter', [ &$this, 'akismet_topic' ], 9 );
34 add_filter( 'wpforo_add_post_data_filter', [ &$this, 'akismet_post' ], 9 );
35 add_filter( 'wpforo_edit_post_data_filter', [ &$this, 'akismet_post' ], 9 );
36 }
37 add_filter( 'wpforo_add_topic_data_filter', [ &$this, 'spam_topic' ], 10 );
38 add_filter( 'wpforo_edit_topic_data_filter', [ &$this, 'spam_topic' ], 10 );
39 add_filter( 'wpforo_add_topic_data_filter', [ &$this, 'spam_post' ], 10 );
40 add_filter( 'wpforo_edit_topic_data_filter', [ &$this, 'spam_post' ], 10 );
41 add_filter( 'wpforo_add_post_data_filter', [ &$this, 'spam_post' ], 10 );
42 add_filter( 'wpforo_edit_post_data_filter', [ &$this, 'spam_post' ], 10 );
43 }
44 if( ! WPF()->perm->can_link() ) {
45 add_filter( 'wpforo_add_topic_data_filter', [ &$this, 'remove_links' ], 20 );
46 add_filter( 'wpforo_edit_topic_data_filter', [ &$this, 'remove_links' ], 20 );
47 add_filter( 'wpforo_add_post_data_filter', [ &$this, 'remove_links' ], 20 );
48 add_filter( 'wpforo_edit_post_data_filter', [ &$this, 'remove_links' ], 20 );
49 }
50 }
51 }
52
53 public function init_list_table() {
54 if( wpfval( $_GET, 'page' ) === wpforo_prefix_slug( 'moderations' ) ) {
55 $this->list_table = new ModerationsListTable();
56 $this->list_table->prepare_items();
57 }
58 }
59
60 public function get_post_status_dname( $status ) {
61 $status = intval( $status );
62
63 return ( isset( $this->post_statuses[ $status ] ) ? $this->post_statuses[ $status ] : $status );
64 }
65
66 public function get_moderations( $args, &$items_count = 0 ) {
67 if( isset( $_GET['filter_by_userid'] ) && wpforo_bigintval( $_GET['filter_by_userid'] ) ) $args['userid'] = wpforo_bigintval( $_GET['filter_by_userid'] );
68 $filter_by_status = intval( ( isset( $_GET['filter_by_status'] ) ? $_GET['filter_by_status'] : 1 ) );
69 $args['status'] = $filter_by_status;
70 if( ! isset( $_GET['order'] ) ) $args['orderby'] = '`created` DESC, `postid` DESC';
71
72 return WPF()->post->get_posts( $args, $items_count );
73 }
74
75 public function search( $needle, $fields = [] ) {
76 $pids = [];
77 if( $posts = WPF()->post->search( $needle ) ) {
78 foreach( $posts as $post ) {
79 $pids[] = $post['postid'];
80 }
81 }
82
83 return $pids;
84 }
85
86 public function post_approve( $postid ) {
87 return WPF()->post->set_status( $postid, 0 );
88 }
89
90 public function post_unapprove( $postid ) {
91 return WPF()->post->set_status( $postid, 1 );
92 }
93
94 public function get_view_url( $arg ) {
95 return WPF()->post->get_url( $arg );
96 }
97
98 public function akismet_topic( $item ) {
99 // Skip for AI-generated content (created by AI Tasks)
100 if ( ! empty( $item['is_ai_generated'] ) ) return $item;
101
102 // Skip unapproved topics
103 if( $item['status'] === 1 ) return $item;
104
105 $post = [];
106 $post['user_ip'] = ( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null );
107 $post['user_agent'] = ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null );
108 $post['referrer'] = ( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null );
109 $post['blog'] = get_option( 'home' );
110 $post['blog_lang'] = get_locale();
111 $post['blog_charset'] = get_option( 'blog_charset' );
112 $post['comment_type'] = 'forum-post';
113
114 if( empty( $item['forumid'] ) ) {
115 $topic = WPF()->topic->get_topic( $item['topicid'] );
116 $item['forumid'] = $topic['forumid'];
117 }
118
119 $post['comment_author'] = WPF()->current_user['user_nicename'];
120 $post['comment_author_email'] = WPF()->current_user['user_email'];
121 $post['comment_author_url'] = WPF()->member->get_profile_url( WPF()->current_userid );
122 $post['comment_post_modified_gmt'] = current_time( 'mysql', 1 );
123 $post['comment_content'] = $item['title'] . " \r\n " . $item['body'];
124 $post['permalink'] = WPF()->forum->get_forum_url( $item['forumid'] );
125
126 $response = Akismet::http_post( Akismet::build_query( $post ), 'comment-check' );
127 if( $response[1] == 'true' ) {
128 $this->ban_for_spam( WPF()->current_userid );
129 $item['status'] = 1;
130
131 // Log to AI moderation table for visibility in admin moderation page
132 $this->save_builtin_moderation_log( [
133 'content_type' => 'topic',
134 'forumid' => $item['forumid'] ?? 0,
135 'userid' => WPF()->current_userid,
136 'moderation_type' => 'spam',
137 'action_taken' => 'unapprove',
138 'action_reason' => 'builtin_akismet',
139 'analysis_summary' => wpforo_phrase( 'Content unapproved by Akismet spam protection.', false ),
140 'content_preview' => isset( $item['title'] ) ? wp_trim_words( $item['title'], 20 ) : null,
141 ] );
142 }
143
144 return $item;
145 }
146
147 public function akismet_post( $item ) {
148 // Skip for AI-generated content (created by AI Tasks)
149 if ( ! empty( $item['is_ai_generated'] ) ) return $item;
150 // Skip unapproved posts
151 if( $item['status'] === 1 ) return $item;
152
153 $post = [];
154 $post['user_ip'] = ( isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : null );
155 $post['user_agent'] = ( isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : null );
156 $post['referrer'] = ( isset( $_SERVER['HTTP_REFERER'] ) ? $_SERVER['HTTP_REFERER'] : null );
157 $post['blog'] = get_option( 'home' );
158 $post['blog_lang'] = get_locale();
159 $post['blog_charset'] = get_option( 'blog_charset' );
160 $post['comment_type'] = 'forum-post';
161
162 $topic = WPF()->topic->get_topic( $item['topicid'] );
163
164 $post['comment_author'] = WPF()->current_user['user_nicename'];
165 $post['comment_author_email'] = WPF()->current_user['user_email'];
166 $post['comment_author_url'] = WPF()->member->get_profile_url( WPF()->current_userid );
167 $post['comment_post_modified_gmt'] = $topic['modified'];
168 $post['comment_content'] = $item['body'];
169 $post['permalink'] = WPF()->topic->get_url( $item['topicid'] );
170
171 $response = Akismet::http_post( Akismet::build_query( $post ), 'comment-check' );
172 if( $response[1] == 'true' ) {
173 $this->ban_for_spam( WPF()->current_userid );
174 $item['status'] = 1;
175
176 // Log to AI moderation table for visibility in admin moderation page
177 $this->save_builtin_moderation_log( [
178 'content_type' => 'post',
179 'topicid' => $item['topicid'] ?? 0,
180 'forumid' => $topic['forumid'] ?? 0,
181 'userid' => WPF()->current_userid,
182 'moderation_type' => 'spam',
183 'action_taken' => 'unapprove',
184 'action_reason' => 'builtin_akismet',
185 'analysis_summary' => wpforo_phrase( 'Content unapproved by Akismet spam protection.', false ),
186 'content_preview' => isset( $item['body'] ) ? wp_trim_words( wp_strip_all_tags( $item['body'] ), 20 ) : null,
187 ] );
188 }
189
190 return $item;
191 }
192
193 public function spam_attachment() {
194 $default_attachments_dir = WPF()->folders['default_attachments']['dir'];
195 if( is_dir( $default_attachments_dir ) ) {
196 if( $handle = opendir( $default_attachments_dir ) ) {
197 while( false !== ( $filename = readdir( $handle ) ) ) {
198 if( $filename == '.' || $filename == '..' ) continue;
199 $file = $default_attachments_dir . DIRECTORY_SEPARATOR . $filename;
200 if( filesize( $file ) === 0 ) continue;
201 $level = $this->spam_file( $filename );
202 if( $level > 2 ) {
203 $link = '<a href="' . admin_url( 'admin.php?page=' . wpforo_prefix_slug( 'tools' ) . '&tab=antispam#spam-files' ) . '"><strong>&gt;&gt;</strong></a>';
204 $phrase = '<strong>SPAM! - </strong>' . sprintf(
205 __(
206 'Probably spam file attachments have been detected by wpForo Spam Control. Please moderate suspected files in wpForo &gt; Settings &gt; Spam Protection.',
207 'wpforo'
208 ),
209 $link
210 );
211 WPF()->notice->add( $phrase, 'error' );
212
213 return true;
214 }
215 }
216 }
217 }
218
219 return false;
220 }
221
222 public function spam_file( $item, $type = 'file' ) {
223 if( ! isset( $item ) || ! $item ) return false;
224 $level = 0;
225 $item = strtolower( (string) $item );
226 $spam_file_phrases = [
227 0 => [ 'watch', 'movie' ],
228 1 => [ 'download', 'free' ],
229 ];
230 if( $type == 'file' ) {
231 $ext_whitelist = wpforo_setting( 'antispam', 'exclude_file_ext' );
232 $ext = strtolower( (string) pathinfo( $item, PATHINFO_EXTENSION ) );
233 $ext_risk = [ 'pdf', 'doc', 'docx', 'txt', 'htm', 'html', 'rtf', 'xml', 'xls', 'xlsx', 'php', 'cgi' ];
234 $ext_risk = wpforo_clear_array( $ext_risk, $ext_whitelist );
235 $ext_high_risk = [ 'php', 'cgi', 'exe' ];
236 $ext_high_risk = wpforo_clear_array( $ext_high_risk, $ext_whitelist );
237 if( in_array( $ext, $ext_risk ) ) {
238 $has_post = WPF()->db->get_var( "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `body` LIKE '%" . esc_sql( $item ) . "%' LIMIT 1" );
239 foreach( $spam_file_phrases as $phrases ) {
240 foreach( $phrases as $phrase ) {
241 if( strpos( (string) $item, $phrase ) !== false ) {
242 if( ! $has_post ) {
243 $level = 4;
244 break 2;
245 } else {
246 $level = 2;
247 break 2;
248 }
249 }
250 }
251 }
252 if( ! $level ) {
253 if( ! $has_post ) {
254 $level = 3;
255 } else {
256 if( in_array( $ext, $ext_high_risk ) ) {
257 $level = 5;
258 } else {
259 $level = 1;
260 }
261 }
262 }
263 }
264
265 return $level;
266 } elseif( $type == 'file-open' ) {
267 $ext = strtolower( (string) pathinfo( $item, PATHINFO_EXTENSION ) );
268 $allow_to_open = [ 'pdf', 'doc', 'docx', 'txt', 'rtf', 'xls', 'xlsx' ];
269 if( in_array( $ext, $allow_to_open ) ) {
270 return true;
271 } else {
272 return false;
273 }
274 }
275
276 return 0;
277 }
278
279 public function spam_topic( $topic ) {
280 if( empty( $topic ) ) return $topic;
281 // Skip for AI-generated content (created by AI Tasks)
282 if ( ! empty( $topic['is_ai_generated'] ) ) return $topic;
283
284 //Skip unapproved topics
285 if( $topic['status'] === 1 ) return $topic;
286
287 if( isset( $topic['title'] ) ) {
288 $item = $topic['title'];
289 } else {
290 return $topic;
291 }
292 $len = wpforo_strlen( $item );
293 if( $len < 10 ) return $topic;
294 $item = strip_tags( (string) $item );
295 $is_similar = false;
296
297 // Get similarity threshold (1-20 setting means 80-99% threshold)
298 $sc_level = ( ! is_null( wpforo_setting( 'antispam', 'spam_filter_level_topic' ) ) ) ? intval( wpforo_setting( 'antispam', 'spam_filter_level_topic' ) ) : 10;
299 if( $sc_level < 1 ) $sc_level = 1;
300 if( $sc_level > 20 ) $sc_level = 20;
301 // Convert to threshold: 1 = 99% similarity required, 20 = 80% similarity required
302 $sc_level = ( 100 - $sc_level );
303
304 // Step 1: Check user's last 3 topics
305 $user_topics = WPF()->topic->get_topics( [
306 'userid' => $topic['userid'],
307 'orderby' => 'created',
308 'order' => 'DESC',
309 'row_count' => 3
310 ] );
311
312 if( ! empty( $user_topics ) ) {
313 foreach( $user_topics as $user_topic ) {
314 $check = strip_tags( (string) $user_topic['title'] );
315 if( $check ) {
316 similar_text( $item, $check, $percent );
317 if( $percent > $sc_level ) {
318 $is_similar = true;
319 break;
320 }
321 }
322 }
323 }
324
325 // Step 2: If not found similar in user's topics, check forum's last 3 topics
326 if( ! $is_similar ) {
327 $forum_topics = WPF()->topic->get_topics( [
328 'orderby' => 'created',
329 'order' => 'DESC',
330 'row_count' => 3
331 ] );
332
333 if( ! empty( $forum_topics ) ) {
334 foreach( $forum_topics as $forum_topic ) {
335 // Skip if it's the same user's topic (already checked above)
336 if( isset( $forum_topic['userid'] ) && $forum_topic['userid'] == $topic['userid'] ) {
337 continue;
338 }
339 $check = strip_tags( (string) $forum_topic['title'] );
340 if( $check ) {
341 similar_text( $item, $check, $percent );
342 if( $percent > $sc_level ) {
343 $is_similar = true;
344 break;
345 }
346 }
347 }
348 }
349 }
350
351 if( $is_similar ) {
352 $this->ban_for_spam( WPF()->current_userid );
353 $topic['status'] = 1;
354
355 // Log to AI moderation table for visibility in admin moderation page
356 $this->save_builtin_moderation_log( [
357 'content_type' => 'topic',
358 'forumid' => $topic['forumid'] ?? 0,
359 'userid' => $topic['userid'] ?? WPF()->current_userid,
360 'moderation_type' => 'spam',
361 'action_taken' => 'unapprove',
362 'action_reason' => 'builtin_similarity_topic',
363 'analysis_summary' => wpforo_phrase( 'Content unapproved by built-in spam protection: Similar topic title detected.', false ),
364 'content_preview' => isset( $topic['title'] ) ? wp_trim_words( $topic['title'], 20 ) : null,
365 ] );
366 }
367
368 return apply_filters( 'wpforo_spam_topic', $topic );
369 }
370
371 public function spam_post( $post ) {
372 if( empty( $post ) ) return $post;
373 // Skip for AI-generated content (created by AI Tasks)
374 if ( ! empty( $post['is_ai_generated'] ) ) return $post;
375
376 //Skip unapproved posts
377 if( $post['status'] === 1 ) return $post;
378
379 if( isset( $post['body'] ) ) {
380 $item = $post['body'];
381 } else {
382 return $post;
383 }
384
385 $item = strip_tags( (string) $item );
386 $is_similar = false;
387
388 // Get similarity threshold (1-20 setting means 80-99% threshold)
389 $sc_level = ! is_null( wpforo_setting( 'antispam', 'spam_filter_level_post' ) ) ? intval( wpforo_setting( 'antispam', 'spam_filter_level_post' ) ) : 10;
390 if( $sc_level < 1 ) $sc_level = 1;
391 if( $sc_level > 20 ) $sc_level = 20;
392 // Convert to threshold: 1 = 99% similarity required, 20 = 80% similarity required
393 $sc_level = ( 100 - $sc_level );
394
395 // Step 1: Check user's last 3 posts
396 $user_posts = WPF()->post->get_posts( [
397 'userid' => $post['userid'],
398 'orderby' => 'created',
399 'order' => 'DESC',
400 'row_count' => 3
401 ] );
402
403 if( ! empty( $user_posts ) ) {
404 foreach( $user_posts as $user_post ) {
405 $check = strip_tags( (string) $user_post['body'] );
406 if( $check ) {
407 similar_text( $item, $check, $percent );
408 if( isset( $percent ) && $percent > $sc_level ) {
409 $is_similar = true;
410 break;
411 }
412 }
413 }
414 }
415
416 // Step 2: If not found similar in user's posts, check forum's last 3 posts
417 if( ! $is_similar ) {
418 $forum_posts = WPF()->post->get_posts( [
419 'orderby' => 'created',
420 'order' => 'DESC',
421 'row_count' => 3
422 ] );
423
424 if( ! empty( $forum_posts ) ) {
425 foreach( $forum_posts as $forum_post ) {
426 // Skip if it's the same user's post (already checked above)
427 if( isset( $forum_post['userid'] ) && $forum_post['userid'] == $post['userid'] ) {
428 continue;
429 }
430 $check = strip_tags( (string) $forum_post['body'] );
431 if( $check ) {
432 similar_text( $item, $check, $percent );
433 if( isset( $percent ) && $percent > $sc_level ) {
434 $is_similar = true;
435 break;
436 }
437 }
438 }
439 }
440 }
441
442 if( $is_similar ) {
443 $this->ban_for_spam( WPF()->current_userid );
444 $post['status'] = 1;
445
446 // Log to AI moderation table for visibility in admin moderation page
447 $this->save_builtin_moderation_log( [
448 'content_type' => 'post',
449 'topicid' => $post['topicid'] ?? 0,
450 'forumid' => $post['forumid'] ?? 0,
451 'userid' => $post['userid'] ?? WPF()->current_userid,
452 'moderation_type' => 'spam',
453 'action_taken' => 'unapprove',
454 'action_reason' => 'builtin_similarity_post',
455 'analysis_summary' => wpforo_phrase( 'Content unapproved by built-in spam protection: Similar post content detected.', false ),
456 'content_preview' => isset( $post['body'] ) ? wp_trim_words( wp_strip_all_tags( $post['body'] ), 20 ) : null,
457 ] );
458 }
459
460 return apply_filters( 'wpforo_spam_post', $post );
461 }
462
463 public function auto_moderate( $item ) {
464
465 if( empty( $item ) ) return $item;
466 // Skip for AI-generated content (created by AI Tasks) - status is set by TaskManager
467 if ( ! empty( $item['is_ai_generated'] ) ) {
468 return $item;
469 }
470 // Skip for bot replies when ai[bot_reply_unapproved] setting is enabled
471 if ( ! empty( $item['is_bot_reply'] ) && wpforo_setting( 'ai', 'bot_reply_unapproved' ) ) {
472 return $item;
473 }
474 if( WPF()->usergroup->can( 'aum' ) ) {
475 $item['status'] = 0;
476
477 return $item;
478 }
479 if( ! WPF()->usergroup->can( 'aup' ) ) {
480 $item['status'] = 1;
481
482 // Log to AI moderation table for visibility in admin moderation page
483 $this->save_builtin_moderation_log( [
484 'content_type' => isset( $item['parentid'] ) ? 'post' : 'topic',
485 'topicid' => $item['topicid'] ?? 0,
486 'forumid' => $item['forumid'] ?? 0,
487 'userid' => $item['userid'] ?? WPF()->current_userid,
488 'moderation_type' => 'spam',
489 'action_taken' => 'unapprove',
490 'action_reason' => 'builtin_no_aup_permission',
491 'analysis_summary' => wpforo_phrase( 'Content unapproved: User requires manual approval (no "Can pass moderation" permission).', false ),
492 'content_preview' => isset( $item['title'] ) ? wp_trim_words( $item['title'], 20 ) : ( isset( $item['body'] ) ? wp_trim_words( wp_strip_all_tags( $item['body'] ), 20 ) : null ),
493 ] );
494
495 return $item;
496 }
497
498 if( WPF()->member->current_user_is_new() ) {
499 if( wpforo_setting( 'antispam', 'unapprove_post_if_user_is_new' ) ) {
500 $item['status'] = 1;
501
502 // Log to AI moderation table for visibility in admin moderation page
503 $this->save_builtin_moderation_log( [
504 'content_type' => isset( $item['parentid'] ) ? 'post' : 'topic',
505 'topicid' => $item['topicid'] ?? 0,
506 'forumid' => $item['forumid'] ?? 0,
507 'userid' => WPF()->current_userid,
508 'moderation_type' => 'spam',
509 'action_taken' => 'unapprove',
510 'action_reason' => 'builtin_new_user',
511 'analysis_summary' => wpforo_phrase( 'Content unapproved: New user requires manual approval.', false ),
512 'content_preview' => isset( $item['title'] ) ? wp_trim_words( $item['title'], 20 ) : ( isset( $item['body'] ) ? wp_trim_words( wp_strip_all_tags( $item['body'] ), 20 ) : null ),
513 ] );
514 } else {
515 $if_link_found = apply_filters( 'wpforo_new_user_post_unapproved_if_link_found', true );
516 if( $if_link_found && isset( $item['body'] ) && isset( $item['title'] ) && $this->has_link( $item ) ) {
517 $item['status'] = 1;
518
519 // Log to AI moderation table for visibility in admin moderation page
520 $this->save_builtin_moderation_log( [
521 'content_type' => isset( $item['parentid'] ) ? 'post' : 'topic',
522 'topicid' => $item['topicid'] ?? 0,
523 'forumid' => $item['forumid'] ?? 0,
524 'userid' => WPF()->current_userid,
525 'moderation_type' => 'spam',
526 'action_taken' => 'unapprove',
527 'action_reason' => 'builtin_new_user_links',
528 'analysis_summary' => wpforo_phrase( 'Content unapproved: New user posted content with external links.', false ),
529 'content_preview' => isset( $item['title'] ) ? wp_trim_words( $item['title'], 20 ) : ( isset( $item['body'] ) ? wp_trim_words( wp_strip_all_tags( $item['body'] ), 20 ) : null ),
530 ] );
531 }
532 $unapproved_all = apply_filters( 'wpforo_new_user_post_unapproved_all', false );
533 if( $unapproved_all && ( ( isset( $item['status'] ) && $item['status'] == 1 ) || $this->has_unapproved( WPF()->current_userid ) ) ) {
534 $this->set_all_unapproved( WPF()->current_userid );
535 $item['status'] = 1;
536 }
537 }
538 }
539
540 // Don't track users as "a user without approved posts" if he/she has no posts.
541 // Just check the number of unapproved posts before initiating this rule,
542 // if no unapproved posts then we don't need to set the first post of this user unapproved.
543 // This checking is already done by New User options when we set "1" post for New User status and turn on the "must be manually approved" option.
544 $must_have_one_approved = apply_filters( 'wpforo_post_moderation_must_have_one_approved', true );
545 if( $must_have_one_approved && $this->has_unapproved( WPF()->current_userid ) ) {
546 // So this rule will only work from the second post,
547 // it'll always keep new posts unapproved if previous posts are not approved yet.
548 if( ! $this->has_approved( WPF()->current_userid ) ) {
549 $item['status'] = 1;
550 }
551 }
552
553 return $item;
554 }
555
556 public function has_approved( $user ) {
557 if( ! $user ) return false;
558 if( isset( $user['userid'] ) ) {
559 $userid = intval( $user['userid'] );
560 } else {
561 $userid = intval( $user );
562 }
563 $has_approved_post = WPF()->db->get_var( "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `userid` = '" . wpforo_bigintval( $userid ) . "' AND `status` = 0 LIMIT 1" );
564 if( $has_approved_post ) {
565 return true;
566 } else {
567 return false;
568 }
569 }
570
571 public function has_unapproved( $user ) {
572 if( empty( $user ) ) return false;
573 if( isset( $user['userid'] ) ) {
574 $userid = intval( $user['userid'] );
575 } else {
576 $userid = intval( $user );
577 }
578 $has_unapproved_post = WPF()->db->get_var( "SELECT `postid` FROM `" . WPF()->tables->posts . "` WHERE `userid` = '" . wpforo_bigintval( $userid ) . "' AND `status` = 1 LIMIT 1" );
579 if( $has_unapproved_post ) {
580 return true;
581 } else {
582 return false;
583 }
584 }
585
586 public function ban_for_spam( $userid ) {
587 if( isset( $userid ) && wpforo_setting( 'antispam', 'spam_user_ban' ) ) {
588 if( ! $this->has_approved( WPF()->current_userid ) ) {
589 WPF()->member->autoban( $userid );
590 }
591 }
592 }
593
594 /**
595 * Save moderation log for built-in spam protection
596 *
597 * This logs built-in antispam actions to the AI moderation table
598 * so they are visible in the wpForo Moderation admin page.
599 *
600 * @param array $data Log data
601 * @return int|false Insert ID on success, false on failure
602 */
603 public function save_builtin_moderation_log( $data ) {
604 $defaults = [
605 'content_type' => '',
606 'content_id' => 0,
607 'topicid' => 0,
608 'forumid' => 0,
609 'userid' => 0,
610 'moderation_type' => 'spam',
611 'score' => 100,
612 'is_flagged' => 1,
613 'confidence' => 1.0,
614 'action_taken' => 'unapprove',
615 'action_reason' => '',
616 'indicators' => null,
617 'analysis_summary' => '',
618 'quality_tier' => 'rule_based',
619 'credits_used' => 0,
620 'context_used' => 0,
621 'indexed_topics_count' => 0,
622 'detection_time_ms' => 0,
623 'content_preview' => null,
624 ];
625
626 $data = wp_parse_args( $data, $defaults );
627
628 // Check if table exists
629 $table_name = WPF()->db->prefix . 'wpforo_ai_moderation';
630 $table_exists = WPF()->db->get_var( "SHOW TABLES LIKE '{$table_name}'" );
631 if ( ! $table_exists ) {
632 return false;
633 }
634
635 $result = WPF()->db->insert(
636 $table_name,
637 [
638 'content_type' => $data['content_type'],
639 'content_id' => (int) $data['content_id'],
640 'topicid' => (int) $data['topicid'],
641 'forumid' => (int) $data['forumid'],
642 'userid' => (int) $data['userid'],
643 'moderation_type' => $data['moderation_type'],
644 'score' => (int) $data['score'],
645 'is_flagged' => (int) $data['is_flagged'],
646 'confidence' => (float) $data['confidence'],
647 'action_taken' => $data['action_taken'],
648 'action_reason' => $data['action_reason'],
649 'indicators' => $data['indicators'] ? wp_json_encode( $data['indicators'] ) : null,
650 'analysis_summary' => $data['analysis_summary'],
651 'quality_tier' => $data['quality_tier'],
652 'credits_used' => (int) $data['credits_used'],
653 'context_used' => (int) $data['context_used'],
654 'indexed_topics_count' => (int) $data['indexed_topics_count'],
655 'detection_time_ms' => (int) $data['detection_time_ms'],
656 'content_preview' => $data['content_preview'],
657 ],
658 [ '%s', '%d', '%d', '%d', '%d', '%s', '%d', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%d', '%d', '%d', '%d', '%s' ]
659 );
660
661 return $result ? WPF()->db->insert_id : false;
662 }
663
664 public function set_all_unapproved( $userid ) {
665 if( isset( $userid ) ) {
666 WPF()->db->update( WPF()->tables->topics, [ 'status' => 1 ], [ 'userid' => intval( $userid ) ], [ '%d' ], [ '%d' ] );
667 WPF()->db->update( WPF()->tables->posts, [ 'status' => 1 ], [ 'userid' => intval( $userid ) ], [ '%d' ], [ '%d' ] );
668 }
669 }
670
671 public function remove_links( $item ) {
672 if( wpfval( $item, 'body' ) ) {
673 $domain = wpforo_get_request_uri();
674 $urls = wp_extract_urls( $item['body'] );
675 if( ! empty( $urls ) ) {
676 foreach( $urls as $k => $url ) {
677 $url = parse_url( $url );
678 if( wpfval( $url, 'host' ) ) {
679 if( strpos( (string) $domain, $url['host'] ) !== false ) unset( $urls[ $k ] );
680 }
681 }
682 if( ! empty( $urls ) ) {
683 $replace = apply_filters( 'wpforo_moderation_replace_body_links', ' <span style="color:#aaa;">' . wpforo_phrase( 'removed link', false, false ) . '</span> ', $item, $urls );
684 $item['body'] = str_replace( $urls, $replace, $item['body'] );
685 do_action( 'wpforo_moderation_remove_body_links', $item, $urls );
686 }
687 }
688 }
689 if( wpfval( $item, 'title' ) ) {
690 $domain = wpforo_get_request_uri();
691 $urls = wp_extract_urls( $item['title'] );
692 if( ! empty( $urls ) ) {
693 foreach( $urls as $k => $url ) {
694 $url = parse_url( $url );
695 if( wpfval( $url, 'host' ) ) {
696 if( strpos( (string) $domain, $url['host'] ) !== false ) unset( $urls[ $k ] );
697 }
698 }
699 if( ! empty( $urls ) ) {
700 $replace = apply_filters( 'wpforo_moderation_replace_title_links', ' -' . wpforo_phrase( 'removed link', false, false ) . '- ', $item, $urls );
701 $item['title'] = str_replace( $urls, $replace, $item['title'] );
702 do_action( 'wpforo_moderation_remove_title_links', $item, $urls );
703 }
704 }
705 }
706
707 return $item;
708 }
709
710 public function has_link( $item ) {
711 $field_urls = [];
712 $domain = wpforo_get_request_uri();
713 $title_urls = wp_extract_urls( $item['title'] );
714 $body_urls = wp_extract_urls( $item['body'] );
715 $user = ( wpfval( $item, 'userid' ) ) ? wpforo_member( $item['userid'] ) : [];
716 $signature_urls = ( wpfval( $user, 'signature' ) ) ? wp_extract_urls( $user['signature'] ) : [];
717
718 if( $fields = wpfval( $item, 'postmetas' ) ) {
719 foreach( $fields as $field ) {
720 if( ! is_scalar( $field ) ) continue;
721 $_urls = wp_extract_urls( $field );
722 if( ! empty( $_urls ) ) {
723 foreach( $_urls as $_url ) $field_urls[] = $_url;
724 }
725 }
726 }
727
728 $urls = array_merge( $title_urls, $body_urls, $signature_urls, $field_urls );
729
730 if( ! empty( $urls ) ) {
731 foreach( $urls as $k => $url ) {
732 $url = parse_url( $url );
733 if( wpfval( $url, 'host' ) ) {
734 if( strpos( (string) $domain, $url['host'] ) !== false ) unset( $urls[ $k ] );
735 }
736 }
737 }
738 if( ! empty( $urls ) ) {
739 return true;
740 }
741
742 return false;
743 }
744
745 public function get_distinct_userids( $status = 1 ) {
746 return WPF()->db->get_col( "SELECT DISTINCT `userid` FROM `" . WPF()->tables->posts . "` WHERE `status` = " . intval( $status ) );
747 }
748
749 public function after_post_report( $postid ) {
750 if( wpforo_setting( 'antispam', 'should_unapprove_after_report' ) ) {
751 $this->post_unapprove( $postid );
752 }
753 }
754 }
755