PluginProbe
wpForo Forum / 3.0.1
wpForo Forum v3.0.1
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.0.1, at classes/Moderation.php

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