| 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 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 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 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 |
|