diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php
index 7dc796b2e..49ebc8af5 100644
--- wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php
@@ -177,14 +177,12 @@ function wporg_themes_suspend_theme() {
 		wp_die( __( 'You are not allowed to suspend this item.', 'wporg-themes' ) );
 	}
 
-	wp_update_post( array(
-		'ID'          => $post_id,
-		'post_status' => 'suspend',
-	) );
+	$redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
+	$page_url     = add_query_arg( [ 'ids' => $post_id, 'count' => 1 ], $redirect_url );
 
-	wporg_themes_remove_wpthemescom( $post->post_name );
+	$redirect_to = add_query_arg( 'bulk_suspend_themes', 1, $page_url );
 
-	wp_redirect( add_query_arg( 'suspended', 1, remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids', 'reinstated' ), wp_get_referer() ) ) );
+	wp_redirect( $redirect_to );
 	exit();
 }
 add_filter( 'admin_action_suspend', 'wporg_themes_suspend_theme' );
@@ -451,3 +449,46 @@ function wporg_themes_save_meta_box_data( $post_id ) {
 	}
 }
 add_action( 'save_post', 'wporg_themes_save_meta_box_data' );
+
+/**
+ * Meta box to choose which version is live.
+ */
+function wporg_themes_suspensions_list_meta_box() {
+	add_meta_box(
+		'wporg_themes_past_suspensions',
+		__( 'Suspensions', 'wporg-themes' ),
+		'wporg_themes_suspensions_meta_box_callback',
+		'repopackage',
+		'normal',
+		'high'
+	);
+}
+add_action( 'add_meta_boxes', 'wporg_themes_suspensions_list_meta_box' );
+
+/**
+ * Displays the content of the `_status` meta box.
+ *
+ * @param WP_Post $post
+ */
+function wporg_themes_suspensions_meta_box_callback( $post ) {
+	$suspensions = get_post_meta( $post->ID, 'suspension_details', true );
+
+	if ( empty( $suspensions ) ) {
+		return;
+	}
+
+	$suspension_details = json_decode( $suspensions, true );
+
+	foreach ( $suspension_details as $past_suspension ) {
+		?>
+		<div>
+		<p><?php echo '<strong>' , __( 'Suspended by: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspended_by'] ); ?></p>
+		<p><?php echo '<strong>' , __( 'Suspended on: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspended_on'] ); ?></p>
+		<p><?php echo '<strong>' , __( 'Reason for suspension: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspension_description'] ); ?></p>
+		<p><?php echo '<strong>' , __( 'Suspension expired: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspension_expiration_date'] ); ?></p>
+		<hr/>
+		</div>
+		<?php
+	}
+}
+
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/assets/suspend-form.css wordpress.org/public_html/wp-content/plugins/theme-directory/assets/suspend-form.css
new file mode 100644
index 000000000..c742b3f10
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/assets/suspend-form.css
@@ -0,0 +1,34 @@
+.bulk-suspension-form {
+  display: flex;
+  margin-top: 20px;
+}
+
+.fields-wrapper {
+  background-color: #fff;
+  width: 100%;
+  border-radius: 10px;
+  padding: 20px;
+  border: 1px solid #7e8993;
+}
+
+.fields-wrapper--metabox {
+  width: auto;
+  border: none;
+  padding: 0;
+}
+
+.fields-row {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  width: 100%;
+  margin-bottom: 20px;
+}
+
+.fields-column {
+  display: flex;
+  flex-direction: column;
+  flex-basis: 100%;
+  flex: 1;
+  margin: 0 10px;
+}
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-repo-package.php wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-repo-package.php
index e59bd9dfe..b2d5f377b 100644
--- wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-repo-package.php
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-repo-package.php
@@ -37,6 +37,11 @@ class WPORG_Themes_Repo_Package extends Repo_Package {
 	 */
 	public function latest_version() {
 		$status = get_post_meta( $this->wp_post->ID, '_status', true );
+
+		if ( empty( $status ) ) {
+			return '';
+		}
+
 		uksort( $status, 'version_compare' );
 
 		// Find if there is a live version, and use that one.
@@ -63,11 +68,11 @@ class WPORG_Themes_Repo_Package extends Repo_Package {
 			case 'version' :
 				return $version;
 			case 'theme-url' :
-				return $this->wp_post->_theme_url[ $version ];
+				return $this->wp_post->_theme_url[ $version ] ?? '';
 			case 'author-url' :
-				return $this->wp_post->_author_url[ $version ];
+				return $this->wp_post->_author_url[ $version ] ?? '';
 			case 'ticket' :
-				return $this->wp_post->_ticket_id[ $version ];
+				return $this->wp_post->_ticket_id[ $version ] ?? '';
 			case 'requires':
 				$values = $this->wp_post->_requires;
 				if ( isset( $values[ $version ] ) ) {
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-suspended-repo-packages.php wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-suspended-repo-packages.php
new file mode 100644
index 000000000..460037b0c
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-suspended-repo-packages.php
@@ -0,0 +1,359 @@
+<?php
+
+include __DIR__ . '/inc/class-wporg-themes-suspended-list-table.php';
+
+/**
+ * Class that handles suspended themes
+ *
+ * Provides a way to see the suspended themes, reason why they were suspended, when they were suspended,
+ * when does the suspension expires and who suspended them.
+ */
+class WPorg_Themes_Suspended_Repo_Packages {
+
+	/**
+	 * Register all the hooks related to the new functionality
+	 */
+	public function register() {
+		add_action( 'admin_menu', [ $this, 'register_suspended_themes_subpages' ] );
+		add_action( 'admin_enqueue_scripts', [ $this, 'add_suspension_scripts' ] );
+		add_action( 'admin_post_suspend_themes', [ $this, 'suspend_themes_action' ] );
+		add_action( 'admin_post_nopriv_suspend_themes', [ $this, 'suspend_themes_action' ] );
+		add_action( 'check_suspended_themes_expiry', [ $this, 'check_suspended_themes_expiry_exec' ] );
+		add_action( 'admin_init', [ $this, 'check_suspended_themes_cron' ] );
+		add_action( 'admin_notices', [ $this, 'show_expired_suspension_themes' ] );
+
+	}
+
+	/**
+	 * Method for registering new menu subpage
+	 */
+	public function register_suspended_themes_subpages() {
+		add_submenu_page(
+			'edit.php?post_type=repopackage',
+			__( 'Suspended Packages', 'wporg-themes' ),
+			__( 'Suspended Packages', 'wporg-themes' ),
+			'manage_options',
+			'suspended-packages-page',
+			[ $this, 'suspended_packages_page_callback' ]
+		);
+
+		add_submenu_page(
+			'edit.php?post_type=repopackage',
+			__( 'Suspension message', 'wporg-themes' ),
+			__( 'Suspension message', 'wporg-themes' ),
+			'manage_options',
+			'suspended-packages-bulk-message-page',
+			[ $this, 'suspended_packages_bulk_message_page_callback' ]
+		);
+	}
+
+	/**
+	 * Enqueue additional JS and CSS on the certain admin pages
+	 *
+	 * @param string $hook Page hook.
+	 */
+	public function add_suspension_scripts( $hook ) {
+		if ( $hook === 'repopackage_page_suspended-packages-bulk-message-page' ) {
+			wp_register_style( 'suspend-form', plugins_url( 'theme-directory/assets/suspend-form.css' ) );
+			wp_enqueue_style('suspend-form');
+
+			wp_enqueue_script( 'jquery-ui-datepicker' );
+			wp_register_style( 'jquery-ui-style', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/smoothness/jquery-ui.css');
+			wp_enqueue_style('jquery-ui-style');
+		}
+	}
+
+	/**
+	 * Callback for rendering the suspended packages page
+	 */
+	public function suspended_packages_page_callback() {
+		$suspended_themes_list = new WPorg_Themes_Suspended_List_Table();
+		?>
+		<div class="wrap">
+			<h2><?php echo get_admin_page_title(); ?></h2>
+			<form method="post" action="<?php echo admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-page&noheader=true' ); ?>">
+				<?php $suspended_themes_list->prepare_items(); ?>
+				<?php $suspended_themes_list->display(); ?>
+			</form>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Callback for bulk message page
+	 */
+	public function suspended_packages_bulk_message_page_callback() {
+		?>
+		<div class="wrap">
+			<h2><?php echo get_admin_page_title(); ?></h2>
+			<?php
+				$theme_ids = $_GET['ids'] ?? '';
+
+				$this->generate_bulk_suspension_form( esc_html( $theme_ids ) );
+			?>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Schedule event twice a day
+	 */
+	public function check_suspended_themes_cron() {
+		if ( ! wp_next_scheduled( 'wporg_themes_check_for_old_themes' ) ) {
+			wp_schedule_event( time(), 'twicedaily', 'check_suspended_themes_expiry' );
+		}
+	}
+
+	/**
+	 * Script that will run periodically to warn of themes which should be reinstated
+	 *
+	 * This will check the suspended themes, filter those whose suspension expiry time has passed
+	 * and add a notification in the admin so that the theme reps can reinstate the theme.
+	 */
+	public function check_suspended_themes_expiry_exec() {
+		// Get a list of all the suspended themes.
+		$suspended_themes = new WP_Query([
+			'post_type'   => 'repopackage',
+			'post_status' => 'suspend',
+		]);
+
+		$themes_to_notify = [];
+
+		if ( $suspended_themes->have_posts() ) {
+			while ( $suspended_themes->have_posts() ) {
+				$suspended_themes->the_post();
+
+				$post_id = get_the_ID();
+
+				$suspension_data = get_post_meta( $post_id, 'suspension_details', true );
+
+				if ( ! empty( $suspension_data ) ) {
+					$suspension_details = json_decode( $suspension_data, true );
+
+					$last_suspension = end( $suspension_details );
+
+					$last_suspension_time = $last_suspension['suspension_expiration_date'];
+
+					if ( time() > strtotime( $last_suspension_time ) ) {
+						$themes_to_notify[] = $post_id;
+					}
+				}
+			}
+
+			wp_reset_postdata();
+		}
+
+		if ( $themes_to_notify ) {
+			set_transient( 'wporg_themes_expired_suspension', wp_json_encode( $themes_to_notify ), DAY_IN_SECONDS );
+		}
+	}
+
+	public function show_expired_suspension_themes() {
+		if ( ! empty( $_GET['reinstated_notice'] ) ) {
+		?>
+		<div id="message" class="updated notice is-dismissible">
+			<p>
+				<?php echo esc_html( $_GET['reinstated_notice'] ); ?>
+			 </p><button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo __(' Dismiss this notice.', 'wporg-themes' ); ?></span></button>
+		</div>
+		<?php
+	}
+
+		$expired_theme_suspensions = get_transient( 'wporg_themes_expired_suspension' );
+
+		if ( empty( $expired_theme_suspensions ) ) {
+			return;
+		}
+
+		$themes_to_unsuspend = json_decode( $expired_theme_suspensions, true );
+
+		$theme_names = array_map( function( $theme_id ) {
+			return '<a href="' . get_edit_post_link( $theme_id ) . '" target="_blank">' . get_the_title( $theme_id ) . '</a>';
+		}, $themes_to_unsuspend );
+
+		$notice = __( 'Themes that need to be reinstated: ', 'wporg-themes' ) . implode( ', ', $theme_names );
+
+		?>
+		<div class="notice notice-warning is-dismissible">
+			<p><?php echo wp_kses_post( $notice ); ?></p>
+		</div>
+		<?php
+
+	}
+
+	/**
+	 * Action ran on theme suspension
+	 *
+	 * This will set theme status to suspend, remove the theme from preview
+	 * and set the trac ticket to suspended status.
+	 */
+	public function suspend_themes_action() {
+		// Safety checks!
+		$post_ids = ! empty( $_POST['post_ids'] ) ? explode( ',', $_POST['post_ids'] ) : [];
+
+		if ( empty( $post_ids )  ) {
+			wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
+			exit();
+		}
+
+		if ( ! wp_verify_nonce( $_POST['bulk_suspend_nonce'], 'bulk_suspend_action' ) ) {
+			wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
+			exit();
+		}
+
+		if ( ! isset( $_POST['bulk_suspend_nonce'] ) && $_POST['bulk_suspend_nonce'] !== 'suspend_themes' ) {
+			wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
+			exit();
+		}
+
+		$suspension_description = $_POST['suspension-description'] ?? '';
+
+		if ( empty( $suspension_description ) ) {
+			$redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
+			$page_url     = add_query_arg( [ 'error' => __( 'Description cannot be empty.', 'wporg-themes' ), 'ids' => $_POST['post_ids'] ], $redirect_url );
+
+			wp_safe_redirect( $page_url );
+			exit();
+		}
+
+		$suspension_expiration_date = $_POST['suspension-date-expiration'] ?? '';
+
+		if ( time() > strtotime( $suspension_expiration_date ) ) {
+			$redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
+			$page_url     = add_query_arg( [ 'error' => __( 'Suspension expiration date cannot be in the past.', 'wporg-themes' ), 'ids' => $_POST['post_ids'] ], $redirect_url );
+
+			wp_safe_redirect( $page_url );
+			exit();
+		}
+
+		$current_user = wp_get_current_user()->display_name;
+		$current_time = date( 'Y-m-d' );
+
+		foreach ( $post_ids as $post_id ) {
+			wp_update_post( array(
+				'ID'          => $post_id,
+				'post_status' => 'suspend',
+			) );
+
+			$theme = new WPORG_Themes_Repo_Package( $post_id );
+
+			// Remove from previewer.
+			wporg_themes_remove_wpthemescom( $theme->post_name );
+
+			$theme->ticket;
+
+			$suspension_data = get_post_meta( $post_id, 'suspension_details', true );
+
+			$suspension_details = [];
+
+			if ( ! empty( $suspension_data ) ) {
+				$suspension_details = json_decode( $suspension_data, true );
+			}
+
+			$data = [
+				'suspended_by'               => $current_user,
+				'suspended_on'               => $current_time,
+				'suspension_description'     => $suspension_description,
+				'suspension_expiration_date' => $suspension_expiration_date,
+			];
+
+			$suspension_details[] = $data;
+
+			update_post_meta( $post_id, 'suspension_details', wp_json_encode( $suspension_details ) );
+
+			// Suspend on trac.
+			$this->suspend_theme_on_trac( $post_id );
+		}
+
+		wp_safe_redirect( esc_url( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-page&noheader=true' ) ) );
+		exit;
+	}
+
+	private function suspend_theme_on_trac( $theme_id ) {
+		if ( ! class_exists( 'Trac' ) ) {
+			require_once ABSPATH . WPINC . '/class-IXR.php';
+			require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
+			require_once __DIR__ . '/lib/class-trac.php';
+		}
+
+		// Check for tickets that were set to live previously.
+		$trac = new Trac( 'themetracbot', THEME_TRACBOT_PASSWORD, 'https://themes.trac.wordpress.org/login/xmlrpc' );
+
+		$tickets = (array) $trac->ticket_query( add_query_arg( array(
+			'keywords'   => '~theme-' . get_post( $theme_id )->post_name,
+			'status'     => 'closed',
+			'resolution' => 'live',
+			'order'      => 'changetime',
+			'desc'       => 1,
+		) ) );
+
+		$last_live = $tickets[0];
+
+		$trac->ticket_update(
+			$last_live,
+			__( 'Theme is suspended for violating Theme Review Team rules', 'wporg-themes' ),
+			[
+				'action' => 'review',
+				'status' => 'suspended',
+			],
+			true
+		);
+	}
+
+	/**
+	 * Generate form that is used for theme suspension
+	 *
+	 * @param string $theme_ids Comma separated list of theme ids to suspend.
+	 */
+	private function generate_bulk_suspension_form( $theme_ids ) {
+
+		$error = $_REQUEST['error'] ?? '';
+		if ( ! empty( $error ) ) {
+			?>
+			<div class="notice error">
+				<?php echo '<p>' , esc_html( $error ) , '</p>' ?>
+			</div>
+			<?php
+		}
+
+		if ( ! empty( $theme_ids ) ) {
+			$notice_text = '';
+			foreach ( explode( ',', $theme_ids ) as $theme_id ) {
+				$theme = new WPORG_Themes_Repo_Package( $theme_id );
+
+				$version = $theme->version;
+				$ticket  = $theme->ticket;
+
+				$notice_text .= '<p>' . __( 'Theme: ', 'wporg-themes' ) . esc_html( $theme->post_title ) . ', ' . esc_html( $version ) .', ' . esc_html( $ticket ) . '</p>';
+			}
+			?>
+			<div class="notice">
+				<p><?php echo __( 'Themes to be suspended:', 'wporg-themes' ); ?></p>
+				<?php echo wp_kses_post( $notice_text ); ?>
+			</div>
+			<?php
+		}
+		?>
+		<form method="post" class="bulk-suspension-form" action="<?php echo esc_attr( admin_url('admin-post.php') ); ?>">
+			<input type="hidden" name="action" value="suspend_themes">
+			<input type="hidden" name="post_ids" value="<?php echo esc_attr( $theme_ids ); ?>">
+			<?php
+			wp_nonce_field( 'bulk_suspend_action', 'bulk_suspend_nonce' );
+			 ?>
+			<div class="fields-wrapper">
+				<div class="fields-row">
+					<div class="fields-column"><strong><label for="suspension-description"><?php echo __( 'Suspension reason', 'wporg-themes' ); ?></label></strong></div>
+					<div class="fields-column"><strong><label for="suspension-date-expiration"><?php echo __( 'Suspension expiration date', 'wporg-themes' ); ?></label></strong></div>
+				</div>
+				<div class="fields-row">
+					<div class="fields-column"><textarea name="suspension-description" id="suspension-description" cols="70" rows="10"></textarea></div>
+					<div class="fields-column"><input type="date" id="suspension-date-expiration" name="suspension-date-expiration" value="" class="suspension-date-expiration-datepicker" /></div>
+				</div>
+				<div class="fields-row">
+					<button type="submit" class="button button-primary"><?php echo __( 'Submit', 'wporg-themes' ); ?></button>
+				</div>
+			</div>
+		</form>
+		<?php
+	}
+}
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-list-table.php wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-list-table.php
new file mode 100644
index 000000000..61c5be496
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-list-table.php
@@ -0,0 +1,1427 @@
+<?php
+/**
+ * Administration API: WP_List_Table class
+ *
+ * @package WordPress
+ * @subpackage List_Table
+ * @since 3.1.0
+ */
+
+/**
+ * Base class for displaying a list of items in an ajaxified HTML table.
+ *
+ * @since 3.1.0
+ * @access private
+ */
+class WPorg_Themes_List_Table {
+
+	/**
+	 * The current list of items.
+	 *
+	 * @since 3.1.0
+	 * @var array
+	 */
+	public $items;
+
+	/**
+	 * Various information about the current table.
+	 *
+	 * @since 3.1.0
+	 * @var array
+	 */
+	protected $_args;
+
+	/**
+	 * Various information needed for displaying the pagination.
+	 *
+	 * @since 3.1.0
+	 * @var array
+	 */
+	protected $_pagination_args = array();
+
+	/**
+	 * The current screen.
+	 *
+	 * @since 3.1.0
+	 * @var object
+	 */
+	protected $screen;
+
+	/**
+	 * Cached bulk actions.
+	 *
+	 * @since 3.1.0
+	 * @var array
+	 */
+	private $_actions;
+
+	/**
+	 * Cached pagination output.
+	 *
+	 * @since 3.1.0
+	 * @var string
+	 */
+	private $_pagination;
+
+	/**
+	 * The view switcher modes.
+	 *
+	 * @since 4.1.0
+	 * @var array
+	 */
+	protected $modes = array();
+
+	/**
+	 * Stores the value returned by ->get_column_info().
+	 *
+	 * @since 4.1.0
+	 * @var array
+	 */
+	protected $_column_headers;
+
+	/**
+	 * {@internal Missing Summary}
+	 *
+	 * @var array
+	 */
+	protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
+
+	/**
+	 * {@internal Missing Summary}
+	 *
+	 * @var array
+	 */
+	protected $compat_methods = array(
+		'set_pagination_args',
+		'get_views',
+		'get_bulk_actions',
+		'bulk_actions',
+		'row_actions',
+		'months_dropdown',
+		'view_switcher',
+		'comments_bubble',
+		'get_items_per_page',
+		'pagination',
+		'get_sortable_columns',
+		'get_column_info',
+		'get_table_classes',
+		'display_tablenav',
+		'extra_tablenav',
+		'single_row_columns',
+	);
+
+	/**
+	 * Constructor.
+	 *
+	 * The child class should call this constructor from its own constructor to override
+	 * the default $args.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param array|string $args {
+	 *     Array or string of arguments.
+	 *
+	 *     @type string $plural   Plural value used for labels and the objects being listed.
+	 *                            This affects things such as CSS class-names and nonces used
+	 *                            in the list table, e.g. 'posts'. Default empty.
+	 *     @type string $singular Singular label for an object being listed, e.g. 'post'.
+	 *                            Default empty
+	 *     @type bool   $ajax     Whether the list table supports Ajax. This includes loading
+	 *                            and sorting data, for example. If true, the class will call
+	 *                            the _js_vars() method in the footer to provide variables
+	 *                            to any scripts handling Ajax events. Default false.
+	 *     @type string $screen   String containing the hook name used to determine the current
+	 *                            screen. If left null, the current screen will be automatically set.
+	 *                            Default null.
+	 * }
+	 */
+	public function __construct( $args = array() ) {
+		$args = wp_parse_args(
+			$args,
+			array(
+				'plural'   => '',
+				'singular' => '',
+				'ajax'     => false,
+				'screen'   => null,
+			)
+		);
+
+		$this->screen = convert_to_screen( $args['screen'] );
+
+		add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
+
+		if ( ! $args['plural'] ) {
+			$args['plural'] = $this->screen->base;
+		}
+
+		$args['plural']   = sanitize_key( $args['plural'] );
+		$args['singular'] = sanitize_key( $args['singular'] );
+
+		$this->_args = $args;
+
+		if ( $args['ajax'] ) {
+			// wp_enqueue_script( 'list-table' );
+			add_action( 'admin_footer', array( $this, '_js_vars' ) );
+		}
+
+		if ( empty( $this->modes ) ) {
+			$this->modes = array(
+				'list'    => __( 'List View' ),
+				'excerpt' => __( 'Excerpt View' ),
+			);
+		}
+	}
+
+	/**
+	 * Make private properties readable for backward compatibility.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param string $name Property to get.
+	 * @return mixed Property.
+	 */
+	public function __get( $name ) {
+		if ( in_array( $name, $this->compat_fields ) ) {
+			return $this->$name;
+		}
+	}
+
+	/**
+	 * Make private properties settable for backward compatibility.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param string $name  Property to check if set.
+	 * @param mixed  $value Property value.
+	 * @return mixed Newly-set property.
+	 */
+	public function __set( $name, $value ) {
+		if ( in_array( $name, $this->compat_fields ) ) {
+			return $this->$name = $value;
+		}
+	}
+
+	/**
+	 * Make private properties checkable for backward compatibility.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param string $name Property to check if set.
+	 * @return bool Whether the property is set.
+	 */
+	public function __isset( $name ) {
+		if ( in_array( $name, $this->compat_fields ) ) {
+			return isset( $this->$name );
+		}
+	}
+
+	/**
+	 * Make private properties un-settable for backward compatibility.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param string $name Property to unset.
+	 */
+	public function __unset( $name ) {
+		if ( in_array( $name, $this->compat_fields ) ) {
+			unset( $this->$name );
+		}
+	}
+
+	/**
+	 * Make private/protected methods readable for backward compatibility.
+	 *
+	 * @since 4.0.0
+	 *
+	 * @param string   $name      Method to call.
+	 * @param array    $arguments Arguments to pass when calling.
+	 * @return mixed|bool Return value of the callback, false otherwise.
+	 */
+	public function __call( $name, $arguments ) {
+		if ( in_array( $name, $this->compat_methods ) ) {
+			return $this->$name( ...$arguments );
+		}
+		return false;
+	}
+
+	/**
+	 * Checks the current user's permissions
+	 *
+	 * @since 3.1.0
+	 * @abstract
+	 */
+	public function ajax_user_can() {
+		die( 'function WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
+	}
+
+	/**
+	 * Prepares the list of items for displaying.
+	 *
+	 * @uses WP_List_Table::set_pagination_args()
+	 *
+	 * @since 3.1.0
+	 * @abstract
+	 */
+	public function prepare_items() {
+		die( 'function WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
+	}
+
+	/**
+	 * An internal method that sets all the necessary pagination arguments
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param array|string $args Array or string of arguments with information about the pagination.
+	 */
+	protected function set_pagination_args( $args ) {
+		$args = wp_parse_args(
+			$args,
+			array(
+				'total_items' => 0,
+				'total_pages' => 0,
+				'per_page'    => 0,
+			)
+		);
+
+		if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
+			$args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
+		}
+
+		// Redirect if page number is invalid and headers are not already sent.
+		if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
+			wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
+			exit;
+		}
+
+		$this->_pagination_args = $args;
+	}
+
+	/**
+	 * Access the pagination args.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $key Pagination argument to retrieve. Common values include 'total_items',
+	 *                    'total_pages', 'per_page', or 'infinite_scroll'.
+	 * @return int Number of items that correspond to the given pagination argument.
+	 */
+	public function get_pagination_arg( $key ) {
+		if ( 'page' === $key ) {
+			return $this->get_pagenum();
+		}
+
+		if ( isset( $this->_pagination_args[ $key ] ) ) {
+			return $this->_pagination_args[ $key ];
+		}
+	}
+
+	/**
+	 * Whether the table has items to display or not
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return bool
+	 */
+	public function has_items() {
+		return ! empty( $this->items );
+	}
+
+	/**
+	 * Message to be displayed when there are no items
+	 *
+	 * @since 3.1.0
+	 */
+	public function no_items() {
+		_e( 'No items found.' );
+	}
+
+	/**
+	 * Displays the search box.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $text     The 'submit' button label.
+	 * @param string $input_id ID attribute value for the search input field.
+	 */
+	public function search_box( $text, $input_id ) {
+		if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
+			return;
+		}
+
+		$input_id = $input_id . '-search-input';
+
+		if ( ! empty( $_REQUEST['orderby'] ) ) {
+			echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
+		}
+		if ( ! empty( $_REQUEST['order'] ) ) {
+			echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
+		}
+		if ( ! empty( $_REQUEST['post_mime_type'] ) ) {
+			echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
+		}
+		if ( ! empty( $_REQUEST['detached'] ) ) {
+			echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
+		}
+		?>
+<p class="search-box">
+	<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
+	<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
+		<?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
+</p>
+		<?php
+	}
+
+	/**
+	 * Get an associative array ( id => link ) with the list
+	 * of views available on this table.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return array
+	 */
+	protected function get_views() {
+		return array();
+	}
+
+	/**
+	 * Display the list of views available on this table.
+	 *
+	 * @since 3.1.0
+	 */
+	public function views() {
+		$views = $this->get_views();
+		/**
+		 * Filters the list of available list table views.
+		 *
+		 * The dynamic portion of the hook name, `$this->screen->id`, refers
+		 * to the ID of the current screen, usually a string.
+		 *
+		 * @since 3.5.0
+		 *
+		 * @param string[] $views An array of available list table views.
+		 */
+		$views = apply_filters( "views_{$this->screen->id}", $views );
+
+		if ( empty( $views ) ) {
+			return;
+		}
+
+		$this->screen->render_screen_reader_content( 'heading_views' );
+
+		echo "<ul class='subsubsub'>\n";
+		foreach ( $views as $class => $view ) {
+			$views[ $class ] = "\t<li class='$class'>$view";
+		}
+		echo implode( " |</li>\n", $views ) . "</li>\n";
+		echo '</ul>';
+	}
+
+	/**
+	 * Get an associative array ( option_name => option_title ) with the list
+	 * of bulk actions available on this table.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return array
+	 */
+	protected function get_bulk_actions() {
+		return array();
+	}
+
+	/**
+	 * Display the bulk actions dropdown.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $which The location of the bulk actions: 'top' or 'bottom'.
+	 *                      This is designated as optional for backward compatibility.
+	 */
+	protected function bulk_actions( $which = '' ) {
+		if ( is_null( $this->_actions ) ) {
+			$this->_actions = $this->get_bulk_actions();
+			/**
+			 * Filters the list table Bulk Actions drop-down.
+			 *
+			 * The dynamic portion of the hook name, `$this->screen->id`, refers
+			 * to the ID of the current screen, usually a string.
+			 *
+			 * This filter can currently only be used to remove bulk actions.
+			 *
+			 * @since 3.5.0
+			 *
+			 * @param string[] $actions An array of the available bulk actions.
+			 */
+			$this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );  // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
+			$two            = '';
+		} else {
+			$two = '2';
+		}
+
+		if ( empty( $this->_actions ) ) {
+			return;
+		}
+
+		echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
+		echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
+		echo '<option value="-1">' . __( 'Bulk Actions' ) . "</option>\n";
+
+		foreach ( $this->_actions as $name => $title ) {
+			$class = 'edit' === $name ? ' class="hide-if-no-js"' : '';
+
+			echo "\t" . '<option value="' . $name . '"' . $class . '>' . $title . "</option>\n";
+		}
+
+		echo "</select>\n";
+
+		submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
+		echo "\n";
+	}
+
+	/**
+	 * Get the current action selected from the bulk actions dropdown.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return string|false The action name or False if no action was selected
+	 */
+	public function current_action() {
+		if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) {
+			return false;
+		}
+
+		if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
+			return $_REQUEST['action'];
+		}
+
+		if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] ) {
+			return $_REQUEST['action2'];
+		}
+
+		return false;
+	}
+
+	/**
+	 * Generate row actions div
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string[] $actions        An array of action links.
+	 * @param bool     $always_visible Whether the actions should be always visible.
+	 * @return string
+	 */
+	protected function row_actions( $actions, $always_visible = false ) {
+		$action_count = count( $actions );
+		$i            = 0;
+
+		if ( ! $action_count ) {
+			return '';
+		}
+
+		$out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
+		foreach ( $actions as $action => $link ) {
+			++$i;
+			( $i == $action_count ) ? $sep = '' : $sep = ' | ';
+			$out                          .= "<span class='$action'>$link$sep</span>";
+		}
+		$out .= '</div>';
+
+		$out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
+
+		return $out;
+	}
+
+	/**
+	 * Display a monthly dropdown for filtering items
+	 *
+	 * @since 3.1.0
+	 *
+	 * @global wpdb      $wpdb      WordPress database abstraction object.
+	 * @global WP_Locale $wp_locale WordPress date and time locale object.
+	 *
+	 * @param string $post_type
+	 */
+	protected function months_dropdown( $post_type ) {
+		global $wpdb, $wp_locale;
+
+		/**
+		 * Filters whether to remove the 'Months' drop-down from the post list table.
+		 *
+		 * @since 4.2.0
+		 *
+		 * @param bool   $disable   Whether to disable the drop-down. Default false.
+		 * @param string $post_type The post type.
+		 */
+		if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
+			return;
+		}
+
+		$extra_checks = "AND post_status != 'auto-draft'";
+		if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
+			$extra_checks .= " AND post_status != 'trash'";
+		} elseif ( isset( $_GET['post_status'] ) ) {
+			$extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
+		}
+
+		$months = $wpdb->get_results(
+			$wpdb->prepare(
+				"
+			SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
+			FROM $wpdb->posts
+			WHERE post_type = %s
+			$extra_checks
+			ORDER BY post_date DESC
+		",
+				$post_type
+			)
+		);
+
+		/**
+		 * Filters the 'Months' drop-down results.
+		 *
+		 * @since 3.7.0
+		 *
+		 * @param object $months    The months drop-down query results.
+		 * @param string $post_type The post type.
+		 */
+		$months = apply_filters( 'months_dropdown_results', $months, $post_type );
+
+		$month_count = count( $months );
+
+		if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) {
+			return;
+		}
+
+		$m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
+		?>
+		<label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label>
+		<select name="m" id="filter-by-date">
+			<option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
+		<?php
+		foreach ( $months as $arc_row ) {
+			if ( 0 == $arc_row->year ) {
+				continue;
+			}
+
+			$month = zeroise( $arc_row->month, 2 );
+			$year  = $arc_row->year;
+
+			printf(
+				"<option %s value='%s'>%s</option>\n",
+				selected( $m, $year . $month, false ),
+				esc_attr( $arc_row->year . $month ),
+				/* translators: 1: Month name, 2: 4-digit year. */
+				sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
+			);
+		}
+		?>
+		</select>
+		<?php
+	}
+
+	/**
+	 * Display a view switcher
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $current_mode
+	 */
+	protected function view_switcher( $current_mode ) {
+		?>
+		<input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
+		<div class="view-switch">
+		<?php
+		foreach ( $this->modes as $mode => $title ) {
+			$classes = array( 'view-' . $mode );
+			if ( $current_mode === $mode ) {
+				$classes[] = 'current';
+			}
+			printf(
+				"<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",
+				esc_url( add_query_arg( 'mode', $mode ) ),
+				implode( ' ', $classes ),
+				$title
+			);
+		}
+		?>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Display a comment count bubble
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param int $post_id          The post ID.
+	 * @param int $pending_comments Number of pending comments.
+	 */
+	protected function comments_bubble( $post_id, $pending_comments ) {
+		$approved_comments = get_comments_number();
+
+		$approved_comments_number = number_format_i18n( $approved_comments );
+		$pending_comments_number  = number_format_i18n( $pending_comments );
+
+		$approved_only_phrase = sprintf(
+			/* translators: %s: Number of comments. */
+			_n( '%s comment', '%s comments', $approved_comments ),
+			$approved_comments_number
+		);
+
+		$approved_phrase = sprintf(
+			/* translators: %s: Number of comments. */
+			_n( '%s approved comment', '%s approved comments', $approved_comments ),
+			$approved_comments_number
+		);
+
+		$pending_phrase = sprintf(
+			/* translators: %s: Number of comments. */
+			_n( '%s pending comment', '%s pending comments', $pending_comments ),
+			$pending_comments_number
+		);
+
+		// No comments at all.
+		if ( ! $approved_comments && ! $pending_comments ) {
+			printf(
+				'<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
+				__( 'No comments' )
+			);
+			// Approved comments have different display depending on some conditions.
+		} elseif ( $approved_comments ) {
+			printf(
+				'<a href="%s" class="post-com-count post-com-count-approved"><span class="comment-count-approved" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
+				esc_url(
+					add_query_arg(
+						array(
+							'p'              => $post_id,
+							'comment_status' => 'approved',
+						),
+						admin_url( 'edit-comments.php' )
+					)
+				),
+				$approved_comments_number,
+				$pending_comments ? $approved_phrase : $approved_only_phrase
+			);
+		} else {
+			printf(
+				'<span class="post-com-count post-com-count-no-comments"><span class="comment-count comment-count-no-comments" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
+				$approved_comments_number,
+				$pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
+			);
+		}
+
+		if ( $pending_comments ) {
+			printf(
+				'<a href="%s" class="post-com-count post-com-count-pending"><span class="comment-count-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></a>',
+				esc_url(
+					add_query_arg(
+						array(
+							'p'              => $post_id,
+							'comment_status' => 'moderated',
+						),
+						admin_url( 'edit-comments.php' )
+					)
+				),
+				$pending_comments_number,
+				$pending_phrase
+			);
+		} else {
+			printf(
+				'<span class="post-com-count post-com-count-pending post-com-count-no-pending"><span class="comment-count comment-count-no-pending" aria-hidden="true">%s</span><span class="screen-reader-text">%s</span></span>',
+				$pending_comments_number,
+				$approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
+			);
+		}
+	}
+
+	/**
+	 * Get the current page number
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return int
+	 */
+	public function get_pagenum() {
+		$pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
+
+		if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
+			$pagenum = $this->_pagination_args['total_pages'];
+		}
+
+		return max( 1, $pagenum );
+	}
+
+	/**
+	 * Get number of items to display on a single page
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $option
+	 * @param int    $default
+	 * @return int
+	 */
+	protected function get_items_per_page( $option, $default = 20 ) {
+		$per_page = (int) get_user_option( $option );
+		if ( empty( $per_page ) || $per_page < 1 ) {
+			$per_page = $default;
+		}
+
+		/**
+		 * Filters the number of items to be displayed on each page of the list table.
+		 *
+		 * The dynamic hook name, $option, refers to the `per_page` option depending
+		 * on the type of list table in use. Possible values include: 'edit_comments_per_page',
+		 * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
+		 * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
+		 * 'edit_{$post_type}_per_page', etc.
+		 *
+		 * @since 2.9.0
+		 *
+		 * @param int $per_page Number of items to be displayed. Default 20.
+		 */
+		return (int) apply_filters( "{$option}", $per_page );
+	}
+
+	/**
+	 * Display the pagination.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $which
+	 */
+	protected function pagination( $which ) {
+		if ( empty( $this->_pagination_args ) ) {
+			return;
+		}
+
+		$total_items     = $this->_pagination_args['total_items'];
+		$total_pages     = $this->_pagination_args['total_pages'];
+		$infinite_scroll = false;
+		if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
+			$infinite_scroll = $this->_pagination_args['infinite_scroll'];
+		}
+
+		if ( 'top' === $which && $total_pages > 1 ) {
+			$this->screen->render_screen_reader_content( 'heading_pagination' );
+		}
+
+		$output = '<span class="displaying-num">' . sprintf(
+			/* translators: %s: Number of items. */
+			_n( '%s item', '%s items', $total_items ),
+			number_format_i18n( $total_items )
+		) . '</span>';
+
+		$current              = $this->get_pagenum();
+		$removable_query_args = wp_removable_query_args();
+
+		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
+
+		$current_url = remove_query_arg( $removable_query_args, $current_url );
+
+		$page_links = array();
+
+		$total_pages_before = '<span class="paging-input">';
+		$total_pages_after  = '</span></span>';
+
+		$disable_first = false;
+		$disable_last  = false;
+		$disable_prev  = false;
+		$disable_next  = false;
+
+		if ( $current == 1 ) {
+			$disable_first = true;
+			$disable_prev  = true;
+		}
+		if ( $current == 2 ) {
+			$disable_first = true;
+		}
+		if ( $current == $total_pages ) {
+			$disable_last = true;
+			$disable_next = true;
+		}
+		if ( $current == $total_pages - 1 ) {
+			$disable_last = true;
+		}
+
+		if ( $disable_first ) {
+			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
+		} else {
+			$page_links[] = sprintf(
+				"<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+				esc_url( remove_query_arg( 'paged', $current_url ) ),
+				__( 'First page' ),
+				'&laquo;'
+			);
+		}
+
+		if ( $disable_prev ) {
+			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
+		} else {
+			$page_links[] = sprintf(
+				"<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+				esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
+				__( 'Previous page' ),
+				'&lsaquo;'
+			);
+		}
+
+		if ( 'bottom' === $which ) {
+			$html_current_page  = $current;
+			$total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
+		} else {
+			$html_current_page = sprintf(
+				"%s<input class='current-page' id='current-page-selector' type='text' name='paged' value='%s' size='%d' aria-describedby='table-paging' /><span class='tablenav-paging-text'>",
+				'<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
+				$current,
+				strlen( $total_pages )
+			);
+		}
+		$html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
+		$page_links[]     = $total_pages_before . sprintf(
+			/* translators: 1: Current page, 2: Total pages. */
+			_x( '%1$s of %2$s', 'paging' ),
+			$html_current_page,
+			$html_total_pages
+		) . $total_pages_after;
+
+		if ( $disable_next ) {
+			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
+		} else {
+			$page_links[] = sprintf(
+				"<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+				esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
+				__( 'Next page' ),
+				'&rsaquo;'
+			);
+		}
+
+		if ( $disable_last ) {
+			$page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
+		} else {
+			$page_links[] = sprintf(
+				"<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
+				esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
+				__( 'Last page' ),
+				'&raquo;'
+			);
+		}
+
+		$pagination_links_class = 'pagination-links';
+		if ( ! empty( $infinite_scroll ) ) {
+			$pagination_links_class .= ' hide-if-js';
+		}
+		$output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
+
+		if ( $total_pages ) {
+			$page_class = $total_pages < 2 ? ' one-page' : '';
+		} else {
+			$page_class = ' no-pages';
+		}
+		$this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
+
+		echo $this->_pagination;
+	}
+
+	/**
+	 * Get a list of columns. The format is:
+	 * 'internal-name' => 'Title'
+	 *
+	 * @since 3.1.0
+	 * @abstract
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+		die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
+	}
+
+	/**
+	 * Get a list of sortable columns. The format is:
+	 * 'internal-name' => 'orderby'
+	 * or
+	 * 'internal-name' => array( 'orderby', true )
+	 *
+	 * The second format will make the initial sorting order be descending
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return array
+	 */
+	protected function get_sortable_columns() {
+		return array();
+	}
+
+	/**
+	 * Gets the name of the default primary column.
+	 *
+	 * @since 4.3.0
+	 *
+	 * @return string Name of the default primary column, in this case, an empty string.
+	 */
+	protected function get_default_primary_column_name() {
+		$columns = $this->get_columns();
+		$column  = '';
+
+		if ( empty( $columns ) ) {
+			return $column;
+		}
+
+		// We need a primary defined so responsive views show something,
+		// so let's fall back to the first non-checkbox column.
+		foreach ( $columns as $col => $column_name ) {
+			if ( 'cb' === $col ) {
+				continue;
+			}
+
+			$column = $col;
+			break;
+		}
+
+		return $column;
+	}
+
+	/**
+	 * Public wrapper for WP_List_Table::get_default_primary_column_name().
+	 *
+	 * @since 4.4.0
+	 *
+	 * @return string Name of the default primary column.
+	 */
+	public function get_primary_column() {
+		return $this->get_primary_column_name();
+	}
+
+	/**
+	 * Gets the name of the primary column.
+	 *
+	 * @since 4.3.0
+	 *
+	 * @return string The name of the primary column.
+	 */
+	protected function get_primary_column_name() {
+		$columns = get_column_headers( $this->screen );
+		$default = $this->get_default_primary_column_name();
+
+		// If the primary column doesn't exist fall back to the
+		// first non-checkbox column.
+		if ( ! isset( $columns[ $default ] ) ) {
+			$default = self::get_default_primary_column_name();
+		}
+
+		/**
+		 * Filters the name of the primary column for the current list table.
+		 *
+		 * @since 4.3.0
+		 *
+		 * @param string $default Column name default for the specific list table, e.g. 'name'.
+		 * @param string $context Screen ID for specific list table, e.g. 'plugins'.
+		 */
+		$column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
+
+		if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
+			$column = $default;
+		}
+
+		return $column;
+	}
+
+	/**
+	 * Get a list of all, hidden and sortable columns, with filter applied
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return array
+	 */
+	protected function get_column_info() {
+		// $_column_headers is already set / cached
+		if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
+			// Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
+			// In 4.3, we added a fourth argument for primary column.
+			$column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
+			foreach ( $this->_column_headers as $key => $value ) {
+				$column_headers[ $key ] = $value;
+			}
+
+			return $column_headers;
+		}
+
+		$columns = get_column_headers( $this->screen );
+		$hidden  = get_hidden_columns( $this->screen );
+
+		$sortable_columns = $this->get_sortable_columns();
+		/**
+		 * Filters the list table sortable columns for a specific screen.
+		 *
+		 * The dynamic portion of the hook name, `$this->screen->id`, refers
+		 * to the ID of the current screen, usually a string.
+		 *
+		 * @since 3.5.0
+		 *
+		 * @param array $sortable_columns An array of sortable columns.
+		 */
+		$_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
+
+		$sortable = array();
+		foreach ( $_sortable as $id => $data ) {
+			if ( empty( $data ) ) {
+				continue;
+			}
+
+			$data = (array) $data;
+			if ( ! isset( $data[1] ) ) {
+				$data[1] = false;
+			}
+
+			$sortable[ $id ] = $data;
+		}
+
+		$primary               = $this->get_primary_column_name();
+		$this->_column_headers = array( $columns, $hidden, $sortable, $primary );
+
+		return $this->_column_headers;
+	}
+
+	/**
+	 * Return number of visible columns
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return int
+	 */
+	public function get_column_count() {
+		list ( $columns, $hidden ) = $this->get_column_info();
+		$hidden                    = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
+		return count( $columns ) - count( $hidden );
+	}
+
+	/**
+	 * Print column headers, accounting for hidden and sortable columns.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @staticvar int $cb_counter
+	 *
+	 * @param bool $with_id Whether to set the id attribute or not
+	 */
+	public function print_column_headers( $with_id = true ) {
+		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
+
+		$current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
+		$current_url = remove_query_arg( 'paged', $current_url );
+
+		if ( isset( $_GET['orderby'] ) ) {
+			$current_orderby = $_GET['orderby'];
+		} else {
+			$current_orderby = '';
+		}
+
+		if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
+			$current_order = 'desc';
+		} else {
+			$current_order = 'asc';
+		}
+
+		if ( ! empty( $columns['cb'] ) ) {
+			static $cb_counter = 1;
+			$columns['cb']     = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
+				. '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
+			$cb_counter++;
+		}
+
+		foreach ( $columns as $column_key => $column_display_name ) {
+			$class = array( 'manage-column', "column-$column_key" );
+
+			if ( in_array( $column_key, $hidden ) ) {
+				$class[] = 'hidden';
+			}
+
+			if ( 'cb' === $column_key ) {
+				$class[] = 'check-column';
+			} elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) ) {
+				$class[] = 'num';
+			}
+
+			if ( $column_key === $primary ) {
+				$class[] = 'column-primary';
+			}
+
+			if ( isset( $sortable[ $column_key ] ) ) {
+				list( $orderby, $desc_first ) = $sortable[ $column_key ];
+
+				if ( $current_orderby === $orderby ) {
+					$order   = 'asc' === $current_order ? 'desc' : 'asc';
+					$class[] = 'sorted';
+					$class[] = $current_order;
+				} else {
+					$order   = $desc_first ? 'desc' : 'asc';
+					$class[] = 'sortable';
+					$class[] = $desc_first ? 'asc' : 'desc';
+				}
+
+				$column_display_name = '<a href="' . esc_url( add_query_arg( compact( 'orderby', 'order' ), $current_url ) ) . '"><span>' . $column_display_name . '</span><span class="sorting-indicator"></span></a>';
+			}
+
+			$tag   = ( 'cb' === $column_key ) ? 'td' : 'th';
+			$scope = ( 'th' === $tag ) ? 'scope="col"' : '';
+			$id    = $with_id ? "id='$column_key'" : '';
+
+			if ( ! empty( $class ) ) {
+				$class = "class='" . join( ' ', $class ) . "'";
+			}
+
+			echo "<$tag $scope $id $class>$column_display_name</$tag>";
+		}
+	}
+
+	/**
+	 * Displays the table.
+	 *
+	 * @since 3.1.0
+	 */
+	public function display() {
+		$singular = $this->_args['singular'];
+
+		$this->display_tablenav( 'top' );
+
+		$this->screen->render_screen_reader_content( 'heading_list' );
+		?>
+<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
+	<thead>
+	<tr>
+		<?php $this->print_column_headers(); ?>
+	</tr>
+	</thead>
+
+	<tbody id="the-list"
+		<?php
+		if ( $singular ) {
+			echo " data-wp-lists='list:$singular'";
+		}
+		?>
+		>
+		<?php $this->display_rows_or_placeholder(); ?>
+	</tbody>
+
+	<tfoot>
+	<tr>
+		<?php $this->print_column_headers( false ); ?>
+	</tr>
+	</tfoot>
+
+</table>
+		<?php
+		$this->display_tablenav( 'bottom' );
+	}
+
+	/**
+	 * Get a list of CSS classes for the WP_List_Table table tag.
+	 *
+	 * @since 3.1.0
+	 *
+	 * @return array List of CSS classes for the table tag.
+	 */
+	protected function get_table_classes() {
+		return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
+	}
+
+	/**
+	 * Generate the table navigation above or below the table
+	 *
+	 * @since 3.1.0
+	 * @param string $which
+	 */
+	protected function display_tablenav( $which ) {
+		if ( 'top' === $which ) {
+			wp_nonce_field( 'bulk-' . $this->_args['plural'] );
+		}
+		?>
+	<div class="tablenav <?php echo esc_attr( $which ); ?>">
+
+		<?php if ( $this->has_items() ) : ?>
+		<div class="alignleft actions bulkactions">
+			<?php $this->bulk_actions( $which ); ?>
+		</div>
+			<?php
+		endif;
+		$this->extra_tablenav( $which );
+		$this->pagination( $which );
+		?>
+
+		<br class="clear" />
+	</div>
+		<?php
+	}
+
+	/**
+	 * Extra controls to be displayed between bulk actions and pagination
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param string $which
+	 */
+	protected function extra_tablenav( $which ) {}
+
+	/**
+	 * Generate the tbody element for the list table.
+	 *
+	 * @since 3.1.0
+	 */
+	public function display_rows_or_placeholder() {
+		if ( $this->has_items() ) {
+			$this->display_rows();
+		} else {
+			echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
+			$this->no_items();
+			echo '</td></tr>';
+		}
+	}
+
+	/**
+	 * Generate the table rows
+	 *
+	 * @since 3.1.0
+	 */
+	public function display_rows() {
+		foreach ( $this->items as $item ) {
+			$this->single_row( $item );
+		}
+	}
+
+	/**
+	 * Generates content for a single row of the table
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param object $item The current item
+	 */
+	public function single_row( $item ) {
+		echo '<tr>';
+		$this->single_row_columns( $item );
+		echo '</tr>';
+	}
+
+	/**
+	 * @param object $item
+	 * @param string $column_name
+	 */
+	protected function column_default( $item, $column_name ) {}
+
+	/**
+	 * @param object $item
+	 */
+	protected function column_cb( $item ) {}
+
+	/**
+	 * Generates the columns for a single row of the table
+	 *
+	 * @since 3.1.0
+	 *
+	 * @param object $item The current item
+	 */
+	protected function single_row_columns( $item ) {
+		list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
+
+		foreach ( $columns as $column_name => $column_display_name ) {
+			$classes = "$column_name column-$column_name";
+			if ( $primary === $column_name ) {
+				$classes .= ' has-row-actions column-primary';
+			}
+
+			if ( in_array( $column_name, $hidden ) ) {
+				$classes .= ' hidden';
+			}
+
+			// Comments column uses HTML in the display name with screen reader text.
+			// Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
+			$data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"';
+
+			$attributes = "class='$classes' $data";
+
+			if ( 'cb' === $column_name ) {
+				echo '<th scope="row" class="check-column">';
+				echo $this->column_cb( $item );
+				echo '</th>';
+			} elseif ( method_exists( $this, '_column_' . $column_name ) ) {
+				echo call_user_func(
+					array( $this, '_column_' . $column_name ),
+					$item,
+					$classes,
+					$data,
+					$primary
+				);
+			} elseif ( method_exists( $this, 'column_' . $column_name ) ) {
+				echo "<td $attributes>";
+				echo call_user_func( array( $this, 'column_' . $column_name ), $item );
+				echo $this->handle_row_actions( $item, $column_name, $primary );
+				echo '</td>';
+			} else {
+				echo "<td $attributes>";
+				echo $this->column_default( $item, $column_name );
+				echo $this->handle_row_actions( $item, $column_name, $primary );
+				echo '</td>';
+			}
+		}
+	}
+
+	/**
+	 * Generates and display row actions links for the list table.
+	 *
+	 * @since 4.3.0
+	 *
+	 * @param object $item        The item being acted upon.
+	 * @param string $column_name Current column name.
+	 * @param string $primary     Primary column name.
+	 * @return string The row actions HTML, or an empty string if the current column is the primary column.
+	 */
+	protected function handle_row_actions( $item, $column_name, $primary ) {
+		return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
+	}
+
+	/**
+	 * Handle an incoming ajax request (called from admin-ajax.php)
+	 *
+	 * @since 3.1.0
+	 */
+	public function ajax_response() {
+		$this->prepare_items();
+
+		ob_start();
+		if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
+			$this->display_rows();
+		} else {
+			$this->display_rows_or_placeholder();
+		}
+
+		$rows = ob_get_clean();
+
+		$response = array( 'rows' => $rows );
+
+		if ( isset( $this->_pagination_args['total_items'] ) ) {
+			$response['total_items_i18n'] = sprintf(
+				/* translators: Number of items. */
+				_n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
+				number_format_i18n( $this->_pagination_args['total_items'] )
+			);
+		}
+		if ( isset( $this->_pagination_args['total_pages'] ) ) {
+			$response['total_pages']      = $this->_pagination_args['total_pages'];
+			$response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
+		}
+
+		die( wp_json_encode( $response ) );
+	}
+
+	/**
+	 * Send required variables to JavaScript land
+	 */
+	public function _js_vars() {
+		$args = array(
+			'class'  => get_class( $this ),
+			'screen' => array(
+				'id'   => $this->screen->id,
+				'base' => $this->screen->base,
+			),
+		);
+
+		printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
+	}
+}
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-suspended-list-table.php wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-suspended-list-table.php
new file mode 100644
index 000000000..182d77320
--- /dev/null
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-suspended-list-table.php
@@ -0,0 +1,266 @@
+<?php
+// If WP_List_Table doesn't exist, fall back to the included one.
+if ( ! class_exists( 'WPorg_Themes_List_Table' ) ) {
+	include __DIR__ . '/class-wporg-themes-list-table.php';
+}
+
+/**
+ * Class WPorg_Themes_Suspended_List_Table
+ *
+ * Extends the extracted WP_List_Table table from core
+ */
+class WPorg_Themes_Suspended_List_Table extends WPorg_Themes_List_Table {
+	/**
+	 * WPorg_Themes_Suspended_List_Table constructor.
+	 */
+	public function __construct() {
+		parent::__construct(
+			array(
+				'singular' => __( 'Suspended theme', 'wporg-themes' ),
+				'plural'   => __( 'Suspended themes', 'wporg-themes' ),
+				'ajax'     => false
+			)
+		);
+	}
+
+	/**
+	 * Prepare the items for the table to process
+	 *
+	 * @return void
+	 */
+	public function prepare_items() {
+
+		$columns  = $this->get_columns();
+		$hidden   = $this->get_hidden_columns();
+		$sortable = $this->get_sortable_columns();
+		$data     = $this->table_data();
+
+
+		usort( $data, array( &$this, 'sort_data' ) );
+
+		$per_page = get_option( 'posts_per_page' );
+
+		$current_page = $this->get_pagenum();
+		$total_items = count( $data );
+
+		$this->set_pagination_args( [
+			'total_items' => $total_items,
+			'per_page'    => $per_page
+		] );
+
+		$data = array_slice( $data, ( ( $current_page-1 ) * $per_page ), $per_page );
+		$this->_column_headers = [ $columns, $hidden, $sortable ];
+		$this->items = $data;
+
+		$this->process_bulk_action();
+	}
+
+	/**
+	 * Markup for checkbox column
+	 *
+	 * @param object $item Current item in the list.
+	 *
+	 * @return string|void
+	 */
+	function column_cb( $item ) {
+		return sprintf(
+			'<label class="screen-reader-text" for="cb-select-%1$d">%2$s</label><input id="cb-select-%1$d" type="checkbox" name="post[]" value="%1$d" />',
+			$item['ID'],
+			$item['title']
+		);
+	}
+
+	/**
+	 * Display custom bulk actions
+	 *
+	 * @return array
+	 */
+	public function get_bulk_actions() {
+		return [
+			'reinstate' => __( 'Reinstate', 'wporg-theme' ),
+		];
+	}
+
+	/**
+	 * Process custom bulk actions
+	 */
+	public function process_bulk_action() {
+		// Security check.
+		if ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['_wpnonce'] ) ) {
+
+			$nonce  = filter_input( INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING );
+			$action = 'bulk-' . $this->_args['plural'];
+
+			if ( ! wp_verify_nonce( $nonce, $action ) ) {
+				wp_die( 'Nope! Security check failed!' );
+			}
+		}
+
+		if ( 'reinstate' === $this->current_action() ) {
+			// Reinstate the themes.
+			$themes_to_reinstate = $_POST['post'];
+
+			foreach ( $themes_to_reinstate as $theme_id ) {
+				wp_update_post( array(
+					'ID'          => (int) $theme_id,
+					'post_status' => 'draft',
+				) );
+
+				/*
+				 * Mark it as reinstated, so the post date doesn't get overwritten when it's
+				 * published again.
+				 */
+				add_post_meta( (int) $theme_id, '_wporg_themes_reinstated', true );
+			}
+
+			$redirect_url = admin_url('edit.php?post_type=repopackage');
+			$page_url     = add_query_arg( [ 'reinstated_notice' => __( 'Themes reinstated.', 'wporg-themes' ) ], $redirect_url );
+
+			wp_safe_redirect( $page_url );
+			exit;
+		}
+	}
+
+	/**
+	 * Override the parent columns method. Defines the columns to use in your listing table
+	 *
+	 * @return array
+	 */
+	public function get_columns() {
+		return [
+			'cb' 					 => '<input type="checkbox" />',
+			'title'                  => __( 'Theme title', 'wporg-theme' ),
+			'ticket'                 => __( 'Ticket ID', 'wporg-theme' ),
+			'author'                 => __( 'Author', 'wporg-theme' ),
+			'suspended-by'           => __( 'Suspended by', 'wporg-theme' ),
+			'suspension-reason'      => __( 'Reason', 'wporg-theme' ),
+			'date-of-suspension'     => __( 'Date', 'wporg-theme' ),
+			'suspension-expiry-date' => __( 'Expiry', 'wporg-theme' ),
+		];
+	}
+
+/**
+	 * Define which columns are hidden
+	 *
+	 * @return array
+	 */
+	public function get_hidden_columns() {
+		return [];
+	}
+
+	/**
+	 * Define the sortable columns
+	 *
+	 * @return array
+	 */
+	public function get_sortable_columns() {
+		return [
+			'title'                  => [ 'title', false ],
+			'date-of-suspension'     => [ 'date-of-suspension', false ],
+			'suspension-expiry-date' => [ 'suspension-expiry-date', false ],
+		];
+	}
+
+	 /**
+	 * Define what data to show on each column of the table
+	 *
+	 * @param  array $item         Data
+	 * @param  string $column_name Current column name
+	 *
+	 * @return mixed
+	 */
+	public function column_default( $item, $column_name ) {
+		switch( $column_name ) {
+			case 'ID':
+			case 'title':
+			case 'ticket':
+			case 'author':
+			case 'suspended-by':
+			case 'suspension-reason':
+			case 'date-of-suspension':
+			case 'suspension-expiry-date':
+				return $item[ $column_name ];
+			default:
+				return print_r( $item, true ) ;
+		}
+	}
+
+	/**
+	 * Get the table data
+	 *
+	 * @return array
+	 */
+	private function table_data() {
+		$suspended_themes = new WP_Query([
+			'post_type'   => 'repopackage',
+			'post_status' => 'suspend',
+		]);
+
+		$suspended_themes_total = $suspended_themes->posts;
+
+		return array_map( static function( $theme ) {
+			$theme_id  = $theme->ID;
+			$author_id = $theme->post_author;
+
+			$theme = new WPORG_Themes_Repo_Package( $theme_id );
+
+			$suspensions = get_post_meta( $theme_id, 'suspension_details', true );
+
+			$suspended_by = '';
+			$suspension_description = '';
+			$suspended_on = '';
+			$suspension_expiration_date = '';
+
+			if ( ! empty( $suspensions ) ) {
+				$suspension_details = json_decode( $suspensions, true );
+				$last_suspension    = end( $suspension_details );
+
+				$suspended_by               = $last_suspension['suspended_by'];
+				$suspension_description     = $last_suspension['suspension_description'];
+				$suspended_on               = $last_suspension['suspended_on'];
+				$suspension_expiration_date = $last_suspension['suspension_expiration_date'];
+			}
+
+
+			return [
+				'ID'                     => $theme_id,
+				'title'                  => get_the_title( $theme_id ),
+				'ticket'                 => $theme->ticket,
+				'author'                 => get_the_author_meta( 'display_name', $author_id ),
+				'suspended-by'           => $suspended_by,
+				'suspension-reason'      => $suspension_description,
+				'date-of-suspension'     => $suspended_on,
+				'suspension-expiry-date' => $suspension_expiration_date,
+			];
+		}, $suspended_themes_total );
+	}
+
+	/**
+	 * Allows you to sort the data by the variables set in the $_GET
+	 *
+	 * @return mixed
+	 */
+	private function sort_data( $a, $b ) {
+		// Set defaults
+		$orderby = 'title';
+		$order   = 'asc';
+
+		// If orderby is set, use this as the sort column
+		if ( ! empty( $_GET['orderby'] ) ) {
+			$orderby = $_GET['orderby'];
+		}
+
+		// If order is set use this as the order
+		if ( !empty( $_GET['order'] ) ) {
+			$order = $_GET['order'];
+		}
+
+		$result = strcmp( $a[$orderby], $b[$orderby] );
+
+		if ( $order === 'asc' ) {
+			return $result;
+		}
+
+		return -$result;
+	}
+}
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php
index 169bdb674..b7830ec42 100644
--- wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php
@@ -31,6 +31,10 @@ class Trac_Sync {
 			'status'     => 'closed',
 			'resolution' => 'not-approved',
 		],
+		'suspended'  => [
+			'status'     => 'suspended',
+			'resolution' => 'not-approved',
+		],
 	];
 
 	/**
diff --git wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php
index e5796871c..b626b01e7 100644
--- wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php
+++ wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php
@@ -34,6 +34,11 @@ include __DIR__ . '/jobs/class-manager.php';
 include __DIR__ . '/jobs/class-trac-sync.php';
 new WordPressdotorg\Theme_Directory\Jobs\Manager();
 
+// Load suspended pages.
+include __DIR__ . '/class-wporg-themes-suspended-repo-packages.php';
+
+( new WPorg_Themes_Suspended_Repo_Packages() )->register();
+
 /**
  * Things to change on activation.
  */
@@ -1297,3 +1302,78 @@ function wporg_themes_get_current_url( $path_only = false ) {
 
 	return $link;
 }
+
+/**
+ * Add suspend bulk action
+ */
+add_action( 'current_screen', 'wporg_themes_bulk_hooks' );
+
+/**
+ * Hooks to current_screen hook, so that we have the WP_Screen defined
+ */
+function wporg_themes_bulk_hooks() {
+	add_filter( 'bulk_actions-edit-repopackage', 'wporg_themes_add_bulk_suspend_option' );
+	add_filter( 'handle_bulk_actions-edit-repopackage', 'wporg_themes_suspend_bulk_action_handler', 10, 3 );
+	add_action( 'admin_notices', 'wporg_themes_suspend_bulk_action_admin_notice' );
+}
+
+/**
+ * Add a new bulk action
+ *
+ * Add the suspend themes action
+ *
+ * @param  array $bulk_actions Array of bulk actions.
+ * @return array               Updated array of bulk actions.
+ */
+function wporg_themes_add_bulk_suspend_option( $bulk_actions ) {
+	$bulk_actions['suspend_themes'] = __( 'Suspend themes', 'wporg-themes' );
+
+	return $bulk_actions;
+}
+
+/**
+ * Add the action handler for the suspend themes action
+ *
+ * @param  string $redirect_to The redirect URL.
+ * @param  string $doaction    The action being taken.
+ * @param  array  $post_ids    The post IDs to take the action on.
+ * @return string              Redirection URL after the action.
+ */
+function wporg_themes_suspend_bulk_action_handler( $redirect_to, $doaction, $post_ids ) {
+	if ( $doaction !== 'suspend_themes' ) {
+		return $redirect_to;
+	}
+
+	$themes_to_suspend = array_filter( $post_ids, static function( $post_id ) {
+		if ( get_post_status( $post_id ) !== 'suspend' ) {
+			return $post_id;
+		}
+	} );
+
+	$number_of_themes_to_suspend = count( $themes_to_suspend );
+
+	$redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
+	$page_url     = add_query_arg( [ 'ids' => implode( ',', $themes_to_suspend ), 'count' => $number_of_themes_to_suspend ], $redirect_url );
+
+	$redirect_to = add_query_arg( 'bulk_suspend_themes', $number_of_themes_to_suspend, $page_url );
+
+	return $redirect_to;
+}
+
+/**
+ * The admin notice after the suspend themes action happened.
+ */
+function wporg_themes_suspend_bulk_action_admin_notice() {
+
+	if ( ! empty( $_REQUEST['bulk_suspend_themes'] ) ) {
+		$suspended_themes_count = (int) $_REQUEST['bulk_suspend_themes'];
+
+		printf( '<div id="message" class="updated notice is-dismissible"><p>' .
+			_n( '%s theme will be suspended.',
+				'%s themes will be suspended.',
+				$suspended_themes_count,
+				'wporg-themes'
+			) . '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">' . __(' Dismiss this notice.', 'wporg-themes' ) . '</span></button></div>',
+			$suspended_themes_count );
+	}
+}
