* @link https://www.fireplugins.com * @copyright Copyright © 2026 FirePlugins All Rights Reserved * @license GNU GPLv3 or later */ namespace FireBox\Core\FB; if (!defined('ABSPATH')) { exit; // Exit if accessed directly. } // Load WP_List_Table if not loaded if (!class_exists('WP_List_Table')) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } use FireBox\Core\Helpers\BoxHelper; class CampaignsList extends \WP_List_Table { private $per_page = 30; private $total_campaigns_data = []; function __construct() { $this->total_campaigns_data = (array) wp_count_posts('firebox', 'readable'); parent::__construct([ 'singular' => 'firebox', 'plural' => 'fireboxes', 'ajax' => false ]); } /** * Gets a list of CSS classes for the WP_List_Table table tag. * * @since 3.1.0 * * @return string[] Array of CSS classes for the table tag. */ protected function get_table_classes() { $mode = get_user_setting('posts_list_mode', 'list'); $mode_class = esc_attr('table-view-' . $mode); return ['widefat', 'striped', $mode_class, $this->_args['plural']]; } protected function get_primary_column_name() { return 'title'; } function get_columns() { return array( 'cb' => '', 'status' => 'Status', 'title' => 'Title', 'views' => 'Views', 'conversions' => 'Conversions', 'conversionrate' => 'Conversion Rate', 'id' => 'ID' ); } function get_sortable_columns() { $sortable_columns = [ 'id' => [ 'id', false ], 'title' => [ 'title', false ], 'views' => [ 'views', false ], 'conversions' => [ 'conversions', false ], 'conversionrate' => [ 'conversionrate', false ], ]; return $sortable_columns; } /** * Renders a checkbox. * * @param object $item * * @return string */ public function column_cb($item) { return sprintf('', esc_attr($item['ID'])); } public function column_status($item) { $payload = [ 'input_class' => ['fpf-toggle-post-status', 'size-small'], 'name' => 'fb_toggle_post_' . $item['ID'], 'extra_atts' => [ 'data-post-id' => $item['ID'] ], 'value' => get_post_status($item['ID']) == 'publish' ? 1 : 0 ]; echo \FPFramework\Helpers\HTML::renderFPToggle($payload); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } public function column_views($item) { return isset($item['analytics']['views']) ? number_format($item['analytics']['views']) : ''; } public function column_conversions($item) { return isset($item['analytics']['conversions']) ? number_format($item['analytics']['conversions']) : ''; } public function column_conversionrate($item) { return isset($item['analytics']['conversionrate']) && $item['analytics']['conversionrate'] ? number_format($item['analytics']['conversionrate'], 1) . '%' : 'n/a'; } /** * Column "title" output. * * @param object $item * * @return void */ public function column_title($item) { $url = admin_url('post.php?post=' . $item['ID'] . '&action=edit'); return '' . $item['label'] . ''; } /** * Processes the bulk actions. * * @return void */ public function process_bulk_action() { // Ensure we have an action if (!$action = $this->current_action()) { return; } // Ensure its a valid action $allowed_actions = $this->get_bulk_actions(); if (!array_key_exists($action, $allowed_actions)) { return; } // Ensure we have IDs $ids = isset($_GET['id']) ? array_map('intval', $_GET['id']) : []; if (!$ids) { return; } // Get nonce $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : ''; if (!$nonce) { return; } // Verify nonce $nonce_action = 'bulk-' . $this->_args['plural']; if (!wp_verify_nonce($nonce, $nonce_action)) { return; } switch ($action) { case 'publish': $this->publishPosts($ids); \FPFramework\Libs\AdminNotice::displaySuccess(sprintf(firebox()->_('FB_X_CAMPAIGNS_HAVE_BEEN_PUBLISHED'), count($ids))); break; case 'unpublish': $this->unpublishPosts($ids); \FPFramework\Libs\AdminNotice::displaySuccess(sprintf(firebox()->_('FB_X_CAMPAIGNS_HAVE_BEEN_UNPUBLISHED'), count($ids))); break; case 'delete': $this->deletePosts($ids); \FPFramework\Libs\AdminNotice::displaySuccess(sprintf(firebox()->_('FB_X_CAMPAIGNS_HAVE_BEEN_DELETED'), count($ids))); break; case 'reset_stats': BoxHelper::resetBoxStats($ids); \FPFramework\Libs\AdminNotice::displaySuccess(sprintf(firebox()->_('FB_X_CAMPAIGNS_HAVE_BEEN_RESET'), count($ids))); break; } } private function publishPosts($ids = []) { if (!$ids) { return; } foreach ($ids as $id) { wp_update_post([ 'ID' => $id, 'post_status' => 'publish' ]); } } private function unpublishPosts($ids = []) { if (!$ids) { return; } foreach ($ids as $id) { wp_update_post([ 'ID' => $id, 'post_status' => 'draft' ]); } } private function deletePosts($ids = []) { if (!$ids) { return; } foreach ($ids as $id) { wp_delete_post($id); } } protected function handle_row_actions($item, $column_name, $primary) { if ($primary !== $column_name) { return ''; } // Restores the more descriptive, specific name for use within this method. $post = $item; $post_type_object = get_post_type_object( $post['post_type'] ); $can_edit_post = current_user_can( 'edit_post', $post['ID'] ); $actions = array(); $title = _draft_or_post_title(); if ( $can_edit_post && 'trash' !== $post['post_status'] ) { $actions['edit'] = sprintf( '%s', get_edit_post_link( $post['ID'] ), /* translators: %s: Post title. */ esc_attr( sprintf( __( 'Edit “%s”', 'firebox' ), $title ) ), __( 'Edit', 'firebox' ) ); } if ( current_user_can( 'delete_post', $post['ID'] ) ) { if ( 'trash' === $post['post_status'] ) { $actions['untrash'] = sprintf( '%s', wp_nonce_url( admin_url( sprintf( $post_type_object->_edit_link . '&action=untrash', $post['ID'] ) ), 'untrash-post_' . $post['ID'] ), /* translators: %s: Post title. */ esc_attr( sprintf( __( 'Restore “%s” from the Trash', 'firebox' ), $title ) ), __( 'Restore', 'firebox' ) ); } elseif ( EMPTY_TRASH_DAYS ) { $actions['trash'] = sprintf( '%s', get_delete_post_link( $post['ID'] ), /* translators: %s: Post title. */ esc_attr( sprintf( __( 'Move “%s” to the Trash', 'firebox' ), $title ) ), _x( 'Trash', 'verb', 'firebox' ) ); } if ( 'trash' === $post['post_status'] || ! EMPTY_TRASH_DAYS ) { $actions['delete'] = sprintf( '%s', get_delete_post_link( $post['ID'], '', true ), /* translators: %s: Post title. */ esc_attr( sprintf( __( 'Delete “%s” permanently', 'firebox' ), $title ) ), __( 'Delete Permanently', 'firebox' ) ); } } if ( is_post_type_viewable( $post_type_object ) ) { if ( in_array( $post['post_status'], array( 'pending', 'draft', 'future' ), true ) ) { if ( $can_edit_post ) { $preview_link = get_preview_post_link( $post['ID'] ); $actions['view'] = sprintf( '%s', esc_url( $preview_link ), /* translators: %s: Post title. */ esc_attr( sprintf( __( 'Preview “%s”', 'firebox' ), $title ) ), __( 'Preview', 'firebox' ) ); } } } // Add 'Duplicate' action $actions['duplicate'] = '' . fpframework()->_('FPF_DUPLICATE') . ''; // Analytics $actions['analytics'] = '' . fpframework()->_('FPF_ANALYTICS') . ''; /** * Check if cookie has been set */ if ((new \FireBox\Core\FB\Cookie(firebox()->box->get($post['ID'])))->exist()) { $actions['clear_cookie'] = '' . firebox()->_('FB_HIDDEN_BY_COOKIE') . ''; } return $this->row_actions( $actions ); } /** * Returns the views. * * @return array */ public function get_views() { $current = $this->getCampaignStatus(); $base_url = admin_url('admin.php?page=firebox-campaigns'); // Base URL $remove = ['status', 'paged', '_wpnonce']; $url = remove_query_arg($remove, $base_url); $count = ' (%d)'; $published = (int) $this->total_campaigns_data['publish']; $drafts = (int) $this->total_campaigns_data['draft']; $total_items = $published + $drafts; // All $all_class = in_array($current, ['', 'all'], true) ? ' class="current"' : ''; $all_count = sprintf($count, esc_attr($total_items)); $all_label = fpframework()->_('FPF_ALL') . $all_count; // Mine $m_class = in_array($current, ['mine'], true) ? ' class="current"' : ''; $m_count = sprintf($count, esc_attr($this->getMineCount())); $m_label = fpframework()->_('FPF_MINE') . $m_count; // Published $p_class = in_array($current, ['published'], true) ? ' class="current"' : ''; $p_count = sprintf($count, esc_attr($published)); $p_label = fpframework()->_('FPF_PUBLISHED') . $p_count; // Drafts $d_class = in_array($current, ['drafts'], true) ? ' class="current"' : ''; $d_count = sprintf($count, esc_attr($drafts)); $d_label = fpframework()->_('FPF_DRAFTS') . $d_count; $views = [ 'all' => sprintf('%s', esc_url($url), $all_class, $all_label), 'mine' => sprintf('%s', esc_url(add_query_arg('status', 'mine', $base_url)), $m_class, $m_label), 'published' => sprintf('%s', esc_url(add_query_arg('status', 'published', $base_url)), $p_class, $p_label), 'drafts' => sprintf('%s', esc_url(add_query_arg('status', 'drafts', $base_url)), $d_class, $d_label), ]; if ($this->total_campaigns_data['trash']) { $t_class = in_array($current, ['trash'], true) ? ' class="current"' : ''; $t_count = sprintf($count, esc_attr((int) $this->total_campaigns_data['trash'])); $t_label = fpframework()->_('FPF_TRASH') . $t_count; $views['trash'] = sprintf('%s', esc_url(add_query_arg('status', 'trash', $base_url)), $t_class, $t_label); } $views['import'] = '' . fpframework()->_('FPF_IMPORT') . ''; return $views; } private function getMineCount() { return (new CampaignsQuery())->getMineCount(); } protected function get_bulk_actions() { return [ 'publish' => __( 'Publish', 'firebox' ), 'unpublish' => __( 'Unpublish', 'firebox' ), 'delete' => __( 'Delete', 'firebox' ), 'fb_export' => __( 'Export', 'firebox' ), 'reset_stats' => __( 'Reset Statistics', 'firebox' ), ]; } /** * Column "id" output. * * @param object $item * * @return void */ public function column_id($item) { $url = admin_url('post.php?post=' . $item['ID'] . '&action=edit'); return '' . $item['ID'] . ''; } private function getCampaignStatus() { return isset($_GET['status']) ? sanitize_key($_GET['status']) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended } function prepare_items() { $status = $this->mapLegacyStatus($this->getCampaignStatus()); $per_page = $this->per_page; $current_page = $this->get_pagenum(); $hidden = []; $columns = $this->get_columns(); $sortable = $this->get_sortable_columns(); $this->_column_headers = [$columns, $hidden, $sortable]; $search = isset($_GET['s']) ? sanitize_text_field(wp_unslash($_GET['s'])) : ''; //phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = isset($_GET['order']) ? sanitize_key($_GET['order']) : 'desc'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended $orderby = isset($_GET['orderby']) ? sanitize_key($_GET['orderby']) : 'id'; //phpcs:ignore WordPress.Security.NonceVerification.Recommended $result = (new CampaignsQuery())->getItems([ 'status' => $status, 'search' => $search, 'orderby' => $orderby, 'order' => $order, 'page' => $current_page, 'per_page' => $per_page, ]); $data = $result['items']; $total_items = $result['found_posts']; if (!$total_items && $this->get_pagenum() !== 1) { if (isset($_REQUEST['post_status']) && in_array($_REQUEST['post_status'], get_post_stati(), true)) //phpcs:ignore WordPress.Security.NonceVerification.Recommended { $total_items = $this->total_campaigns_data[sanitize_key(wp_unslash($_REQUEST['post_status']))]; //phpcs:ignore WordPress.Security.NonceVerification.Recommended } elseif (isset($_GET['author']) && get_current_user_id() === (int) $_GET['author']) //phpcs:ignore WordPress.Security.NonceVerification.Recommended { $total_items = $this->getMineCount(); } else { $total_items = array_sum($this->total_campaigns_data); // Subtract post types that are not included in the admin all list. foreach (get_post_stati(['show_in_admin_all_list' => false]) as $state) { $total_items -= $this->total_campaigns_data[$state]; } } } $this->set_pagination_args([ 'total_items' => $total_items, 'per_page' => $per_page ]); $this->items = $data; } /** * Maps the legacy `status` query-arg values used by this list table's * URLs (published/drafts) to the CampaignsQuery status enum (publish/draft). * * @param string $status * * @return string */ private function mapLegacyStatus($status) { $map = [ '' => 'all', 'all' => 'all', 'mine' => 'mine', 'published' => 'publish', 'drafts' => 'draft', 'trash' => 'trash', ]; return $map[$status] ?? 'all'; } private function getTotalItems() { $status = $this->getCampaignStatus(); $total = 0; switch ($status) { case 'mine': $total = $this->getMineCount(); break; case 'published': $total = $this->total_campaigns_data['publish']; break; case 'drafts': $total = $this->total_campaigns_data['draft']; break; case 'trash': $total = $this->total_campaigns_data['trash']; break; default: $total = $this->total_campaigns_data['publish'] + $this->total_campaigns_data['draft']; break; } return $total; } }