PluginProbe
wpForo Forum / 3.0.8
wpForo Forum v3.0.8
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 / admin / listtables / Moderations.php

Moderations.php in wpForo Forum 3.0.8, at admin/listtables/Moderations.php

686 lines 29.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\admin\listtables;
4
5 use WP_List_Table;
6
7 require_once( ABSPATH . 'wp-admin/includes/template.php' );
8 require_once( ABSPATH . 'wp-admin/includes/screen.php' );
9 require_once( ABSPATH . 'wp-admin/includes/class-wp-screen.php' );
10 require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
11
12 class Moderations extends WP_List_Table {
13
14 public $wpfitems_count;
15
16 /**
17 * Cache for moderation data to avoid repeated queries
18 */
19 private $moderation_cache = [];
20
21 /** ************************************************************************
22 * REQUIRED. Set up a constructor that references the parent constructor. We
23 * use the parent reference to set some default configs.
24 ***************************************************************************/
25 function __construct() {
26 //Set parent defaults
27 parent::__construct(
28 [
29 'singular' => 'moderation', //singular name of the listed records
30 'plural' => 'moderations', //plural name of the listed records
31 'ajax' => false, //does this table support ajax?
32 'screen' => 'wpForoModerations',
33 ]
34 );
35
36 }
37
38
39 /** ************************************************************************
40 * Recommended. This method is called when the parent class can't find a method
41 * specifically build for a given column. Generally, it's recommended to include
42 * one method for each column you want to render, keeping your package class
43 * neat and organized. For example, if the class needs to process a column
44 * named 'title', it would first see if a method named $this->column_title()
45 * exists - if it does, that method will be used. If it doesn't, this one will
46 * be used. Generally, you should try to use custom column methods as much as
47 * possible.
48 *
49 * Since we have defined a column_title() method later on, this method doesn't
50 * need to concern itself with any column with a name of 'title'. Instead, it
51 * needs to handle everything else.
52 *
53 * For more detailed insight into how columns are handled, take a look at
54 * WP_List_Table::single_row_columns()
55 *
56 * @param array $item A singular item (one full row's worth of data)
57 * @param string $column_name The name/slug of the column to be processed
58 *
59 * @return string Text or HTML to be placed inside the column <td>
60 **************************************************************************/
61 function column_default( $item, $column_name ) {
62 switch( $column_name ) {
63 case 'title':
64 return apply_filters( 'wpforo_admin_listtables_moderations_column_title', $item[ $column_name ], $item );
65 case 'userid':
66 $userdata = get_userdata( $item[ $column_name ] );
67 $display_name = ! empty( $userdata->user_nicename ) ? urldecode( $userdata->user_nicename ) : $item[ $column_name ];
68
69 // Check if user is banned
70 $member = WPF()->member->get_member( $item[ $column_name ] );
71 $is_banned = ( ! empty( $member ) && isset( $member['status'] ) && $member['status'] === 'banned' );
72
73 if( $is_banned ) {
74 return '<span style="color: #dc3545; font-weight: 500;" title="' . esc_attr__( 'Banned', 'wpforo' ) . '">' . esc_html( $display_name ) . '</span>';
75 }
76
77 return esc_html( $display_name );
78 case 'is_first_post':
79 return ( $item[ $column_name ] ) ? __( 'TOPIC', 'wpforo' ) : __( 'REPLY', 'wpforo' );
80 case 'private':
81 return ( $item[ $column_name ] ) ? __( 'YES', 'wpforo' ) : __( 'NO', 'wpforo' );
82 default:
83 return $item[ $column_name ];
84 }
85 }
86
87
88 /** ************************************************************************
89 * Recommended. This is a custom column method and is responsible for what
90 * is rendered in any column with a name/slug of 'title'. Every time the class
91 * needs to render a column, it first looks for a method named
92 * column_{$column_title} - if it exists, that method is run. If it doesn't
93 * exist, column_default() is called instead.
94 *
95 * This example also illustrates how to implement rollover actions. Actions
96 * should be an associative array formatted as 'slug'=>'link html' - and you
97 * will need to generate the URLs yourself. You could even ensure the links
98 *
99 *
100 * @param array $item A singular item (one full row's worth of data)
101 *
102 * @return string Text to be placed inside the column <td> (movie title only)
103 **************************************************************************@see WP_List_Table::::single_row_columns()
104 */
105 function column_postid( $item ) {
106 $vhref = WPF()->moderation->get_view_url( $item['postid'] );
107 //Build row actions
108 $actions = [ 'view' => '<a href="' . $vhref . '" target="_blank">' . __( 'View', 'wpforo' ) . '</a>' ];
109 if( $this->get_filter_by_status_var() ) {
110 $ahref = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&postid=%3$s', wpforo_prefix_slug( 'moderations' ), 'dashboard_post_approve', $item['postid'] ) ), 'wpforo-approve-post-' . $item['postid'] );
111 $actions['wpfapprove'] = '<a href="' . $ahref . '">' . __( 'Approve', 'wpforo' ) . '</a>';
112 } else {
113 $uhref = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&postid=%3$s', wpforo_prefix_slug( 'moderations' ), 'dashboard_post_unapprove', $item['postid'] ) ), 'wpforo-unapprove-post-' . $item['postid'] );
114 $actions['wpfunapprove'] = '<a href="' . $uhref . '">' . __( 'Unapprove', 'wpforo' ) . '</a>';
115 }
116
117 $dhref = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&postid=%3$s', wpforo_prefix_slug( 'moderations' ), 'dashboard_post_delete', $item['postid'] ) ), 'wpforo-delete-post-' . $item['postid'] );
118 $actions['delete'] = '<a onclick="return confirm(\'' . __( "Are you sure you want to DELETE this item?", 'wpforo' ) . '\');" href="' . $dhref . '">' . __( 'Delete', 'wpforo' ) . '</a>';
119
120 // Ban/Unban User action - show based on user's current ban status
121 if( ! empty( $item['userid'] ) && $item['userid'] > 0 && WPF()->usergroup->can( 'bm' ) && intval( $item['userid'] ) !== WPF()->current_userid ) {
122 $member = WPF()->member->get_member( $item['userid'] );
123 $is_banned = ( ! empty( $member ) && isset( $member['status'] ) && $member['status'] === 'banned' );
124
125 if( $is_banned ) {
126 $unban_url = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&userid=%3$s', wpforo_prefix_slug( 'members' ), 'user_unban', $item['userid'] ) ), 'wpforo-user-unban-' . $item['userid'] );
127 $actions['ban_user'] = '<a style="white-space:nowrap; color:#006600;" onclick="return confirm(\'' . __( "Are you sure you want to UNBAN this user?", 'wpforo' ) . '\');" href="' . esc_url( $unban_url ) . '">' . __( 'Unban&nbsp;User', 'wpforo' ) . '</a>';
128 } else {
129 $ban_url = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&userid=%3$s', wpforo_prefix_slug( 'members' ), 'user_ban', $item['userid'] ) ), 'wpforo-user-ban-' . $item['userid'] );
130 $actions['ban_user'] = '<a style="white-space:nowrap; color:orange;" onclick="return confirm(\'' . __( "Are you sure you want to BAN this user?", 'wpforo' ) . '\');" href="' . esc_url( $ban_url ) . '">' . __( 'Ban&nbsp;User', 'wpforo' ) . '</a>';
131 }
132 }
133
134 // Delete User action - links to WordPress delete user page
135 if( ! empty( $item['userid'] ) && $item['userid'] > 0 && WPF()->usergroup->can( 'dm' ) && intval( $item['userid'] ) !== WPF()->current_userid ) {
136 $delete_user_url = wp_nonce_url( admin_url( 'users.php?action=delete&user=' . intval( $item['userid'] ) ), 'bulk-users' );
137 $actions['delete_user'] = '<a style="white-space:nowrap;" onclick="return confirm(\'' . __( "Are you sure you want to DELETE this USER? This will open the WordPress user deletion page where you can choose to delete or reassign their content.", 'wpforo' ) . '\');" href="' . esc_url( $delete_user_url ) . '">' . __( 'Delete&nbsp;User', 'wpforo' ) . '</a>';
138 }
139
140 $actions = apply_filters( 'wpforo_admin_listtables_moderations_actions', $actions, $item );
141
142 //Return the title contents
143 return sprintf(
144 '%1$s %2$s',
145 /*$1%s*/ $item['postid'],
146 /*$2%s*/ $this->row_actions( $actions )
147 );
148 }
149
150
151 /** ************************************************************************
152 * REQUIRED if displaying checkboxes or using bulk actions! The 'cb' column
153 * is given special treatment when columns are processed. It ALWAYS needs to
154 * have its own method.
155 *
156 * @param array $item A singular item (one full row's worth of data)
157 *
158 * @return string Text to be placed inside the column <td> (movie title only)
159 **************************************************************************@see WP_List_Table::::single_row_columns()
160 */
161 function column_cb( $item ) {
162 return sprintf(
163 '<input type="checkbox" name="%1$s[]" value="%2$s" />',
164 /*$1%s*/ 'postids', //Let's simply repurpose the table's singular label ("movie")
165 /*$2%s*/ $item['postid'] //The value of the checkbox should be the record's id
166 );
167 }
168
169
170 /** ************************************************************************
171 * REQUIRED! This method dictates the table's columns and titles. This should
172 * return an array where the key is the column slug (and class) and the value
173 * is the column's title text. If you need a checkbox for bulk actions, refer
174 * to the $columns array below.
175 *
176 * The 'cb' column is treated differently than the rest. If including a checkbox
177 * column in your table you must create a column_cb() method. If you don't need
178 * to bulk actions or checkboxes, simply leave the 'cb' entry out of your array.
179 *
180 * @return array An associative array containing column information: 'slugs'=>'Visible Titles'
181 **************************************************************************@see WP_List_Table::::single_row_columns()
182 */
183 function get_columns() {
184 return [
185 'cb' => '<input type="checkbox" />', //Render a checkbox instead of text
186 'postid' => __( 'ID', 'wpforo' ),
187 'title' => __( 'Title', 'wpforo' ),
188 'is_first_post' => __( 'Type', 'wpforo' ),
189 'userid' => __( 'Created By', 'wpforo' ),
190 'created' => __( 'Created', 'wpforo' ),
191 'private' => __( 'Private', 'wpforo' ),
192 ];
193 }
194
195
196 /** ************************************************************************
197 * Optional. If you want one or more columns to be sortable (ASC/DESC toggle),
198 * you will need to register it here. This should return an array where the
199 * key is the column that needs to be sortable, and the value is db column to
200 * sort by. Often, the key and value will be the same, but this is not always
201 * the case (as the value is a column name from the database, not the list table).
202 *
203 * This method merely defines which columns should be sortable and makes them
204 * clickable - it does not handle the actual sorting. You still need to detect
205 * the ORDERBY and ORDER querystring variables within prepare_items() and sort
206 * your data accordingly (usually by modifying your query).
207 *
208 * @return array An associative array containing all the columns that should be sortable: 'slugs'=>array('data_values',bool)
209 **************************************************************************/
210 function get_sortable_columns() {
211 return [
212 'postid' => [ 'postid', false ], //true means it's already sorted
213 'title' => [ 'title', false ],
214 'is_first_post' => [ 'is_first_post', false ],
215 'userid' => [ 'userid', false ],
216 'created' => [ 'created', false ],
217 'private' => [ 'private', false ],
218 ];
219 }
220
221
222 /** ************************************************************************
223 * Optional. If you need to include bulk actions in your list table, this is
224 * the place to define them. Bulk actions are an associative array in the format
225 * 'slug'=>'Visible Title'
226 *
227 * If this method returns an empty value, no bulk action will be rendered. If
228 * you specify any bulk actions, the bulk actions box will be rendered with
229 * the table automatically on display().
230 *
231 * Also note that list tables are not automatically wrapped in <form> elements,
232 * so you will need to create those manually in order for bulk actions to function.
233 *
234 * @return array An associative array containing all the bulk actions: 'slugs'=>'Visible Titles'
235 **************************************************************************/
236 function get_bulk_actions() {
237 $bulk_actions = [];
238 if( $this->get_filter_by_status_var() ) {
239 $bulk_actions['approve'] = __( 'Approve', 'wpforo' );
240 } else {
241 $bulk_actions['unapprove'] = __( 'Unapprove', 'wpforo' );
242 }
243 $bulk_actions['delete'] = __( 'Delete', 'wpforo' );
244
245 return $bulk_actions;
246 }
247
248
249 /** ************************************************************************
250 * REQUIRED! This is where you prepare your data for display. This method will
251 * usually be used to query the database, sort and filter the data, and generally
252 * get it ready to be displayed. At a minimum, we should set $this->items and
253 * $this->set_pagination_args(), although the following properties and methods
254 * are frequently interacted with here...
255 *
256 * @uses $this->_column_headers
257 * @uses $this->items
258 * @uses $this->get_columns()
259 * @uses $this->get_sortable_columns()
260 * @uses $this->get_pagenum()
261 * @uses $this->set_pagination_args()
262 **************************************************************************/
263 function prepare_items() {
264 /**
265 * First, lets decide how many records per page to show
266 */
267 $per_page = wpforo_get_option( 'count_per_page', 10 );
268
269
270 /**
271 * REQUIRED. Now we need to define our column headers. This includes a complete
272 * array of columns to be displayed (slugs & titles), a list of columns
273 * to keep hidden, and a list of columns that are sortable. Each of these
274 * can be defined in another method (as we've done here) before being
275 * used to build the value for our _column_headers property.
276 */
277 $columns = $this->get_columns();
278 $hidden = [];
279 $sortable = $this->get_sortable_columns();
280
281
282 /**
283 * REQUIRED. Finally, we build an array to be used by the class for column
284 * headers. The $this->_column_headers property takes an array which contains
285 * 3 other arrays. One for all columns, one for hidden columns, and one
286 * for sortable columns.
287 */
288 $this->_column_headers = [ $columns, $hidden, $sortable ];
289
290 /**
291 * REQUIRED. Now we can add our *sorted* data to the items' property, where
292 * it can be used by the rest of the class.
293 */
294 $args = [ 'check_private' => false, 'status' => $this->get_filter_by_status_var(), 'orderby' => '`created` DESC, `postid` DESC' ];
295 if( $s = wpfval( $_REQUEST, 's' ) ) {
296 $args['include'] = WPF()->moderation->search( $s );
297 }
298 $filter_by_userid = $this->get_filter_by_userid_var();
299 $orderby = wpfval( $_REQUEST, 'orderby' );
300 $order = strtoupper( (string) wpfval( $_REQUEST, 'order' ) );
301 if( $filter_by_userid !== - 1 ) $args['userid'] = $filter_by_userid;
302
303 if( $type = $this->get_filter_by_type_var() ){
304 $args['is_first_post'] = $type === 'topic';
305 }
306
307 if( array_key_exists( $orderby, $sortable ) ) $args['orderby'] = sanitize_text_field( $orderby );
308 if( in_array( $order, [ 'ASC', 'DESC' ] ) ) $args['order'] = sanitize_text_field( $order );
309
310 $paged = $this->get_pagenum();
311 $args['offset'] = ( $paged - 1 ) * $per_page;
312 $args['row_count'] = $per_page;
313
314 $items_count = 0;
315 $this->items = ( isset( $args['include'] ) && empty( $args['include'] ) ? [] : WPF()->post->get_posts( $args, $items_count ) );
316
317 $this->wpfitems_count = $items_count;
318
319 $this->set_pagination_args( [
320 'total_items' => $items_count, //WE have to calculate the total number of items
321 'per_page' => $per_page, //WE have to determine how many items to show on a page
322 'total_pages' => ceil( $items_count / $per_page ) //WE have to calculate the total number of pages
323 ] );
324 }
325
326 public function get_filter_by_status_var() {
327 $filter_by_status = wpfval( $_REQUEST, 'filter_by_status' );
328 if( ! is_null( $filter_by_status ) && $filter_by_status !== '-1' ) {
329 $status = intval( $filter_by_status );
330 } else {
331 $status = 1;
332 }
333
334 return $status;
335 }
336
337 public function get_filter_by_type_var() {
338 $filter_by_type = wpfval( $_REQUEST, 'filter_by_type' );
339
340 return in_array( $filter_by_type, [ 'topic', 'reply' ], true ) ? $filter_by_type : '';
341 }
342
343 public function get_filter_by_userid_var() {
344 $filter_by_userid = wpfval( $_REQUEST, 'filter_by_userid' );
345 if( ! is_null( $filter_by_userid ) && $filter_by_userid !== '-1' ) {
346 $userid = wpforo_bigintval( $filter_by_userid );
347 } else {
348 $userid = - 1;
349 }
350
351 return $userid;
352 }
353
354 public function users_dropdown() {
355 ?>
356 <label>
357 <select name="filter_by_userid">
358 <option value="-1">-- <?php _e( 'All Users', 'wpforo' ); ?> --</option>
359
360 <?php
361 if( $userids = WPF()->moderation->get_distinct_userids( $this->get_filter_by_status_var() ) ) {
362 $current_userid = $this->get_filter_by_userid_var();
363 foreach( $userids as $userid ) {
364 $userid = wpforo_bigintval( $userid );
365 $userdata = get_userdata( $userid );
366 ?>
367 <option value="<?php echo $userid ?>" <?php echo( $current_userid === $userid ? 'selected' : '' ) ?> > <?php echo( ! empty( $userdata->user_nicename ) ? urldecode( $userdata->user_nicename ) : $userid ) ?> </option>
368 <?php
369 }
370 }
371 ?>
372 </select>
373 </label>
374 <?php
375 }
376
377 public function status_dropdown() {
378 $filter_by_status = $this->get_filter_by_status_var();
379 if( $statuses = WPF()->moderation->post_statuses ) : ?>
380 <label>
381 <select name="filter_by_status">
382 <?php foreach( $statuses as $key => $status ) : ?>
383 <option value="<?php echo esc_attr( $key ) ?>" <?php echo( $filter_by_status === $key ? 'selected' : '' ) ?>><?php echo esc_html( $status ) ?></option>
384 <?php endforeach; ?>
385 </select>
386 </label>
387 <?php
388 endif;
389 }
390
391 public function type_dropdown() {
392 $filter_by_type = $this->get_filter_by_type_var();
393 ?>
394 <label>
395 <select name="filter_by_type">
396 <option value="">-- <?php _e('All', 'wpforo') ?> --</option>
397 <option value="topic" <?php echo( $filter_by_type === 'topic' ? 'selected' : '' ) ?>><?php _e('Topic', 'wpforo') ?></option>
398 <option value="reply" <?php echo( $filter_by_type === 'reply' ? 'selected' : '' ) ?>><?php _e('Reply', 'wpforo') ?></option>
399 </select>
400 </label>
401 <?php
402 }
403
404 /**
405 * Override single_row to add moderation report row after each post row
406 *
407 * @param array $item The current item
408 */
409 public function single_row( $item ) {
410 // Output the regular row
411 parent::single_row( $item );
412
413 // Only show moderation report row for unapproved posts
414 if( $this->get_filter_by_status_var() !== 1 ) {
415 return;
416 }
417
418 // Get moderation data for this post
419 $moderation_data = $this->get_moderation_data( $item );
420
421 // Always render the report row to maintain odd/even striping
422 // Empty rows will be hidden via CSS
423 $this->render_moderation_report_row( $item, $moderation_data );
424 }
425
426 /**
427 * Get moderation data for a post
428 *
429 * @param array $item Post item data
430 * @return array|null Moderation data or null
431 */
432 private function get_moderation_data( $item ) {
433 $postid = intval( $item['postid'] );
434
435 // Check cache first
436 if( isset( $this->moderation_cache[ $postid ] ) ) {
437 return $this->moderation_cache[ $postid ];
438 }
439
440 $result = [];
441
442 // Determine content type
443 $is_first_post = ! empty( $item['is_first_post'] );
444 $content_type = $is_first_post ? 'topic' : 'post';
445 $content_id = $is_first_post ? ( $item['topicid'] ?? $item['postid'] ) : $item['postid'];
446
447 // Get AI Moderation data (if AI Moderation is active)
448 if( class_exists( '\wpforo\classes\AIContentModeration' ) && ! empty( WPF()->ai_content_moderation ) ) {
449 $ai_logs = WPF()->ai_content_moderation->get_moderation_logs( $content_type, (int) $content_id );
450 if( ! empty( $ai_logs ) ) {
451 $result['ai_moderation'] = $ai_logs;
452 }
453 }
454
455 // Check for wpForo built-in antispam (this is simple - just a flag that post was unapproved by antispam)
456 // Built-in antispam doesn't store detailed data, but we can check if it was likely antispam-based
457 // by looking at whether user is new or if spam patterns were detected
458 if( empty( $result['ai_moderation'] ) && ! empty( $item['userid'] ) ) {
459 $new_user_max_posts = wpforo_setting( 'antispam', 'new_user_max_posts' );
460 if( $new_user_max_posts ) {
461 $user_posts = WPF()->member->member_approved_posts( $item['userid'] );
462 if( $user_posts <= $new_user_max_posts ) {
463 // Likely caught by built-in antispam for new users
464 $result['builtin_antispam'] = [
465 'reason' => 'new_user',
466 'label' => __( 'New User Filter', 'wpforo' ),
467 'description' => __( 'Post was automatically held for moderation because the author is a new user.', 'wpforo' ),
468 ];
469 }
470 }
471 }
472
473 // Cache the result
474 $this->moderation_cache[ $postid ] = $result;
475
476 return $result;
477 }
478
479 /**
480 * Render the moderation report row
481 *
482 * @param array $item Post item data
483 * @param array $moderation_data Moderation data (can be empty)
484 */
485 private function render_moderation_report_row( $item, $moderation_data ) {
486 $columns_count = count( $this->get_columns() );
487 $has_data = ! empty( $moderation_data );
488 $row_class = 'wpf-moderation-report-row' . ( $has_data ? '' : ' wpf-moderation-report-empty' );
489 ?>
490 <tr class="<?php echo esc_attr( $row_class ); ?>">
491 <?php if( $has_data ) : ?>
492 <td colspan="<?php echo esc_attr( $columns_count ); ?>" class="wpf-moderation-report-cell">
493 <div class="wpf-moderation-report-container">
494 <?php
495 // Display AI Moderation reports
496 if( ! empty( $moderation_data['ai_moderation'] ) ) {
497 foreach( $moderation_data['ai_moderation'] as $log ) {
498 $this->render_ai_moderation_report( $log );
499 }
500 }
501
502 // Display built-in antispam info
503 if( ! empty( $moderation_data['builtin_antispam'] ) ) {
504 $this->render_builtin_antispam_report( $moderation_data['builtin_antispam'] );
505 }
506 ?>
507 </div>
508 </td>
509 <?php else : ?>
510 <td colspan="<?php echo esc_attr( $columns_count ); ?>"></td>
511 <?php endif; ?>
512 </tr>
513 <?php
514 }
515
516 /**
517 * Render AI moderation report
518 *
519 * @param array $log Moderation log data
520 */
521 private function render_ai_moderation_report( $log ) {
522 $score = (int) ( $log['score'] ?? 0 );
523 $confidence = (float) ( $log['confidence'] ?? 0 );
524 $mod_type = $log['moderation_type'] ?? 'spam';
525 $action = $log['action_taken'] ?? 'none';
526 $summary = $log['analysis_summary'] ?? '';
527 $indicators = $log['indicators'] ?? [];
528 $quality = $log['quality_tier'] ?? 'fast';
529 $credits = (int) ( $log['credits_used'] ?? 0 );
530 $created = $log['created'] ?? '';
531 $context_used = ! empty( $log['context_used'] );
532 $is_ai = ( $quality !== 'rule_based' );
533
534 // Decode indicators if string
535 if( is_string( $indicators ) && ! empty( $indicators ) ) {
536 $indicators = json_decode( $indicators, true ) ?: [];
537 }
538
539 // Moderation type config
540 $type_config = [
541 'spam' => [
542 'label' => $is_ai ? __( 'Spam Detection', 'wpforo' ) : __( 'Auto Moderation', 'wpforo' ),
543 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>',
544 'color' => $is_ai ? '#d63384' : '#0d6efd',
545 ],
546 'toxicity' => [
547 'label' => __( 'Toxicity Detection', 'wpforo' ),
548 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
549 'color' => '#fd7e14',
550 ],
551 'compliance' => [
552 'label' => __( 'Policy Compliance', 'wpforo' ),
553 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/></svg>',
554 'color' => '#6f42c1',
555 ],
556 'flood' => [
557 'label' => __( 'Auto Moderation', 'wpforo' ),
558 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>',
559 'color' => '#0d6efd',
560 ],
561 ];
562
563 $config = $type_config[ $mod_type ] ?? $type_config['spam'];
564
565 // Status class and label
566 $status_class = 'clean';
567 $status_label = __( 'Clean', 'wpforo' );
568 if( $score >= 85 ) {
569 $status_class = 'detected';
570 $status_label = __( 'Detected', 'wpforo' );
571 } elseif( $score >= 70 ) {
572 $status_class = 'suspected';
573 $status_label = __( 'Suspected', 'wpforo' );
574 } elseif( $score >= 51 ) {
575 $status_class = 'uncertain';
576 $status_label = __( 'Uncertain', 'wpforo' );
577 }
578
579 // Action labels
580 $action_labels = [
581 'none' => __( 'No action', 'wpforo' ),
582 'approve' => __( 'Auto-approved', 'wpforo' ),
583 'auto_approve' => __( 'Auto-approved', 'wpforo' ),
584 'unapprove' => __( 'Unapproved', 'wpforo' ),
585 'unapprove_ban' => __( 'Unapproved + Banned', 'wpforo' ),
586 'delete_author' => __( 'Deleted + Banned', 'wpforo' ),
587 ];
588 $action_label = $action_labels[ $action ] ?? $action;
589
590 // Quality labels
591 $quality_labels = [
592 'fast' => __( 'Fast', 'wpforo' ),
593 'balanced' => __( 'Balanced', 'wpforo' ),
594 'advanced' => __( 'Advanced', 'wpforo' ),
595 'premium' => __( 'Premium', 'wpforo' ),
596 'rule_based' => __( 'Rule-based', 'wpforo' ),
597 ];
598 $quality_label = $quality_labels[ $quality ] ?? $quality;
599 ?>
600 <div class="wpf-mod-report wpf-mod-report-ai wpf-mod-status-<?php echo esc_attr( $status_class ); ?>" data-type="<?php echo esc_attr( $mod_type ); ?>">
601 <div class="wpf-mod-report-type">
602 <span class="wpf-mod-type-icon" style="color: <?php echo esc_attr( $config['color'] ); ?>">
603 <?php echo $config['icon']; ?>
604 </span>
605 <span class="wpf-mod-type-label"><?php echo esc_html( $config['label'] ); ?></span>
606 </div>
607 <div class="wpf-mod-report-content">
608 <div class="wpf-mod-report-header">
609 <span class="wpf-mod-status wpf-mod-status-<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
610 <?php if( $is_ai ) : ?>
611 <span class="wpf-mod-score"><?php printf( __( 'Score: %d%%', 'wpforo' ), $score ); ?></span>
612 <span class="wpf-mod-confidence"><?php printf( __( 'Confidence: %d%%', 'wpforo' ), round( $confidence * 100 ) ); ?></span>
613 <?php else : ?>
614 <span class="wpf-mod-score"><?php _e( 'Score: -', 'wpforo' ); ?></span>
615 <?php endif; ?>
616 <span class="wpf-mod-action"><?php printf( __( 'Action: %s', 'wpforo' ), $action_label ); ?></span>
617 </div>
618
619 <?php if( ! empty( $summary ) ) : ?>
620 <div class="wpf-mod-report-summary">
621 <strong><?php _e( 'Summary:', 'wpforo' ); ?></strong> <?php echo esc_html( $summary ); ?>
622 </div>
623 <?php endif; ?>
624
625 <?php if( ! empty( $indicators ) && is_array( $indicators ) ) : ?>
626 <div class="wpf-mod-report-indicators">
627 <strong><?php _e( 'Indicators:', 'wpforo' ); ?></strong>
628 <ul class="wpf-mod-indicator-list">
629 <?php foreach( $indicators as $indicator ) : ?>
630 <li class="wpf-mod-indicator wpf-mod-severity-<?php echo esc_attr( strtolower( $indicator['severity'] ?? 'medium' ) ); ?>">
631 <span class="wpf-mod-indicator-category"><?php echo esc_html( $indicator['category'] ?? '' ); ?></span>
632 <?php if( ! empty( $indicator['description'] ) ) : ?>
633 <span class="wpf-mod-indicator-desc"><?php echo esc_html( $indicator['description'] ); ?></span>
634 <?php endif; ?>
635 </li>
636 <?php endforeach; ?>
637 </ul>
638 </div>
639 <?php endif; ?>
640
641 <div class="wpf-mod-report-meta">
642 <?php if( $is_ai ) : ?>
643 <span class="wpf-mod-quality"><?php printf( __( 'AI executor: %s', 'wpforo' ), $quality_label ); ?></span>
644 <?php if( $credits > 0 ) : ?>
645 <span class="wpf-mod-credits"><?php printf( _n( '%d credit', '%d credits', $credits, 'wpforo' ), $credits ); ?></span>
646 <?php endif; ?>
647 <?php if( $context_used ) : ?>
648 <span class="wpf-mod-context"><?php _e( 'Context-aware', 'wpforo' ); ?></span>
649 <?php endif; ?>
650 <?php endif; ?>
651 <?php if( ! empty( $created ) ) : ?>
652 <span class="wpf-mod-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $created ) ) ); ?></span>
653 <?php endif; ?>
654 </div>
655 </div>
656 </div>
657 <?php
658 }
659
660 /**
661 * Render built-in antispam report
662 *
663 * @param array $data Antispam data
664 */
665 private function render_builtin_antispam_report( $data ) {
666 ?>
667 <div class="wpf-mod-report wpf-mod-report-builtin">
668 <div class="wpf-mod-report-type">
669 <span class="wpf-mod-type-icon" style="color: #0d6efd;">
670 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
671 </span>
672 <span class="wpf-mod-type-label"><?php _e( 'wpForo Antispam', 'wpforo' ); ?></span>
673 </div>
674 <div class="wpf-mod-report-content">
675 <div class="wpf-mod-report-header">
676 <span class="wpf-mod-status wpf-mod-status-builtin"><?php echo esc_html( $data['label'] ); ?></span>
677 </div>
678 <div class="wpf-mod-report-summary">
679 <?php echo esc_html( $data['description'] ); ?>
680 </div>
681 </div>
682 </div>
683 <?php
684 }
685 }
686