Making WordPress.org

Ticket #4875: 4875.diff

File 4875.diff, 68.0 KB (added by dingo_d, 4 years ago)
  • wordpress.org/public_html/wp-content/plugins/theme-directory/admin-edit.php

    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
    function wporg_themes_suspend_theme() { 
    177177                wp_die( __( 'You are not allowed to suspend this item.', 'wporg-themes' ) );
    178178        }
    179179
    180         wp_update_post( array(
    181                 'ID'          => $post_id,
    182                 'post_status' => 'suspend',
    183         ) );
     180        $redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
     181        $page_url     = add_query_arg( [ 'ids' => $post_id, 'count' => 1 ], $redirect_url );
    184182
    185         wporg_themes_remove_wpthemescom( $post->post_name );
     183        $redirect_to = add_query_arg( 'bulk_suspend_themes', 1, $page_url );
    186184
    187         wp_redirect( add_query_arg( 'suspended', 1, remove_query_arg( array( 'trashed', 'untrashed', 'deleted', 'ids', 'reinstated' ), wp_get_referer() ) ) );
     185        wp_redirect( $redirect_to );
    188186        exit();
    189187}
    190188add_filter( 'admin_action_suspend', 'wporg_themes_suspend_theme' );
    function wporg_themes_save_meta_box_data( $post_id ) { 
    451449        }
    452450}
    453451add_action( 'save_post', 'wporg_themes_save_meta_box_data' );
     452
     453/**
     454 * Meta box to choose which version is live.
     455 */
     456function wporg_themes_suspensions_list_meta_box() {
     457        add_meta_box(
     458                'wporg_themes_past_suspensions',
     459                __( 'Suspensions', 'wporg-themes' ),
     460                'wporg_themes_suspensions_meta_box_callback',
     461                'repopackage',
     462                'normal',
     463                'high'
     464        );
     465}
     466add_action( 'add_meta_boxes', 'wporg_themes_suspensions_list_meta_box' );
     467
     468/**
     469 * Displays the content of the `_status` meta box.
     470 *
     471 * @param WP_Post $post
     472 */
     473function wporg_themes_suspensions_meta_box_callback( $post ) {
     474        $suspensions = get_post_meta( $post->ID, 'suspension_details', true );
     475
     476        if ( empty( $suspensions ) ) {
     477                return;
     478        }
     479
     480        $suspension_details = json_decode( $suspensions, true );
     481
     482        foreach ( $suspension_details as $past_suspension ) {
     483                ?>
     484                <div>
     485                <p><?php echo '<strong>' , __( 'Suspended by: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspended_by'] ); ?></p>
     486                <p><?php echo '<strong>' , __( 'Suspended on: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspended_on'] ); ?></p>
     487                <p><?php echo '<strong>' , __( 'Reason for suspension: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspension_description'] ); ?></p>
     488                <p><?php echo '<strong>' , __( 'Suspension expired: ', 'wporg-themes' ), '</strong>' , esc_html( $past_suspension['suspension_expiration_date'] ); ?></p>
     489                <hr/>
     490                </div>
     491                <?php
     492        }
     493}
     494
  • new file wordpress.org/public_html/wp-content/plugins/theme-directory/assets/suspend-form.css

    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
    - +  
     1.bulk-suspension-form {
     2  display: flex;
     3  margin-top: 20px;
     4}
     5
     6.fields-wrapper {
     7  background-color: #fff;
     8  width: 100%;
     9  border-radius: 10px;
     10  padding: 20px;
     11  border: 1px solid #7e8993;
     12}
     13
     14.fields-wrapper--metabox {
     15  width: auto;
     16  border: none;
     17  padding: 0;
     18}
     19
     20.fields-row {
     21  display: flex;
     22  flex-direction: row;
     23  flex-wrap: wrap;
     24  width: 100%;
     25  margin-bottom: 20px;
     26}
     27
     28.fields-column {
     29  display: flex;
     30  flex-direction: column;
     31  flex-basis: 100%;
     32  flex: 1;
     33  margin: 0 10px;
     34}
  • wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-repo-package.php

    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
    class WPORG_Themes_Repo_Package extends Repo_Package { 
    3737         */
    3838        public function latest_version() {
    3939                $status = get_post_meta( $this->wp_post->ID, '_status', true );
     40
     41                if ( empty( $status ) ) {
     42                        return '';
     43                }
     44
    4045                uksort( $status, 'version_compare' );
    4146
    4247                // Find if there is a live version, and use that one.
    class WPORG_Themes_Repo_Package extends Repo_Package { 
    6368                        case 'version' :
    6469                                return $version;
    6570                        case 'theme-url' :
    66                                 return $this->wp_post->_theme_url[ $version ];
     71                                return $this->wp_post->_theme_url[ $version ] ?? '';
    6772                        case 'author-url' :
    68                                 return $this->wp_post->_author_url[ $version ];
     73                                return $this->wp_post->_author_url[ $version ] ?? '';
    6974                        case 'ticket' :
    70                                 return $this->wp_post->_ticket_id[ $version ];
     75                                return $this->wp_post->_ticket_id[ $version ] ?? '';
    7176                        case 'requires':
    7277                                $values = $this->wp_post->_requires;
    7378                                if ( isset( $values[ $version ] ) ) {
  • new file wordpress.org/public_html/wp-content/plugins/theme-directory/class-wporg-themes-suspended-repo-packages.php

    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
    - +  
     1<?php
     2
     3include __DIR__ . '/inc/class-wporg-themes-suspended-list-table.php';
     4
     5/**
     6 * Class that handles suspended themes
     7 *
     8 * Provides a way to see the suspended themes, reason why they were suspended, when they were suspended,
     9 * when does the suspension expires and who suspended them.
     10 */
     11class WPorg_Themes_Suspended_Repo_Packages {
     12
     13        /**
     14         * Register all the hooks related to the new functionality
     15         */
     16        public function register() {
     17                add_action( 'admin_menu', [ $this, 'register_suspended_themes_subpages' ] );
     18                add_action( 'admin_enqueue_scripts', [ $this, 'add_suspension_scripts' ] );
     19                add_action( 'admin_post_suspend_themes', [ $this, 'suspend_themes_action' ] );
     20                add_action( 'admin_post_nopriv_suspend_themes', [ $this, 'suspend_themes_action' ] );
     21                add_action( 'check_suspended_themes_expiry', [ $this, 'check_suspended_themes_expiry_exec' ] );
     22                add_action( 'admin_init', [ $this, 'check_suspended_themes_cron' ] );
     23                add_action( 'admin_notices', [ $this, 'show_expired_suspension_themes' ] );
     24
     25        }
     26
     27        /**
     28         * Method for registering new menu subpage
     29         */
     30        public function register_suspended_themes_subpages() {
     31                add_submenu_page(
     32                        'edit.php?post_type=repopackage',
     33                        __( 'Suspended Packages', 'wporg-themes' ),
     34                        __( 'Suspended Packages', 'wporg-themes' ),
     35                        'manage_options',
     36                        'suspended-packages-page',
     37                        [ $this, 'suspended_packages_page_callback' ]
     38                );
     39
     40                add_submenu_page(
     41                        'edit.php?post_type=repopackage',
     42                        __( 'Suspension message', 'wporg-themes' ),
     43                        __( 'Suspension message', 'wporg-themes' ),
     44                        'manage_options',
     45                        'suspended-packages-bulk-message-page',
     46                        [ $this, 'suspended_packages_bulk_message_page_callback' ]
     47                );
     48        }
     49
     50        /**
     51         * Enqueue additional JS and CSS on the certain admin pages
     52         *
     53         * @param string $hook Page hook.
     54         */
     55        public function add_suspension_scripts( $hook ) {
     56                if ( $hook === 'repopackage_page_suspended-packages-bulk-message-page' ) {
     57                        wp_register_style( 'suspend-form', plugins_url( 'theme-directory/assets/suspend-form.css' ) );
     58                        wp_enqueue_style('suspend-form');
     59
     60                        wp_enqueue_script( 'jquery-ui-datepicker' );
     61                        wp_register_style( 'jquery-ui-style', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/smoothness/jquery-ui.css');
     62                        wp_enqueue_style('jquery-ui-style');
     63                }
     64        }
     65
     66        /**
     67         * Callback for rendering the suspended packages page
     68         */
     69        public function suspended_packages_page_callback() {
     70                $suspended_themes_list = new WPorg_Themes_Suspended_List_Table();
     71                ?>
     72                <div class="wrap">
     73                        <h2><?php echo get_admin_page_title(); ?></h2>
     74                        <form method="post" action="<?php echo admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-page&noheader=true' ); ?>">
     75                                <?php $suspended_themes_list->prepare_items(); ?>
     76                                <?php $suspended_themes_list->display(); ?>
     77                        </form>
     78                </div>
     79                <?php
     80        }
     81
     82        /**
     83         * Callback for bulk message page
     84         */
     85        public function suspended_packages_bulk_message_page_callback() {
     86                ?>
     87                <div class="wrap">
     88                        <h2><?php echo get_admin_page_title(); ?></h2>
     89                        <?php
     90                                $theme_ids = $_GET['ids'] ?? '';
     91
     92                                $this->generate_bulk_suspension_form( esc_html( $theme_ids ) );
     93                        ?>
     94                </div>
     95                <?php
     96        }
     97
     98        /**
     99         * Schedule event twice a day
     100         */
     101        public function check_suspended_themes_cron() {
     102                if ( ! wp_next_scheduled( 'wporg_themes_check_for_old_themes' ) ) {
     103                        wp_schedule_event( time(), 'twicedaily', 'check_suspended_themes_expiry' );
     104                }
     105        }
     106
     107        /**
     108         * Script that will run periodically to warn of themes which should be reinstated
     109         *
     110         * This will check the suspended themes, filter those whose suspension expiry time has passed
     111         * and add a notification in the admin so that the theme reps can reinstate the theme.
     112         */
     113        public function check_suspended_themes_expiry_exec() {
     114                // Get a list of all the suspended themes.
     115                $suspended_themes = new WP_Query([
     116                        'post_type'   => 'repopackage',
     117                        'post_status' => 'suspend',
     118                ]);
     119
     120                $themes_to_notify = [];
     121
     122                if ( $suspended_themes->have_posts() ) {
     123                        while ( $suspended_themes->have_posts() ) {
     124                                $suspended_themes->the_post();
     125
     126                                $post_id = get_the_ID();
     127
     128                                $suspension_data = get_post_meta( $post_id, 'suspension_details', true );
     129
     130                                if ( ! empty( $suspension_data ) ) {
     131                                        $suspension_details = json_decode( $suspension_data, true );
     132
     133                                        $last_suspension = end( $suspension_details );
     134
     135                                        $last_suspension_time = $last_suspension['suspension_expiration_date'];
     136
     137                                        if ( time() > strtotime( $last_suspension_time ) ) {
     138                                                $themes_to_notify[] = $post_id;
     139                                        }
     140                                }
     141                        }
     142
     143                        wp_reset_postdata();
     144                }
     145
     146                if ( $themes_to_notify ) {
     147                        set_transient( 'wporg_themes_expired_suspension', wp_json_encode( $themes_to_notify ), DAY_IN_SECONDS );
     148                }
     149        }
     150
     151        public function show_expired_suspension_themes() {
     152                if ( ! empty( $_GET['reinstated_notice'] ) ) {
     153                ?>
     154                <div id="message" class="updated notice is-dismissible">
     155                        <p>
     156                                <?php echo esc_html( $_GET['reinstated_notice'] ); ?>
     157                         </p><button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php echo __(' Dismiss this notice.', 'wporg-themes' ); ?></span></button>
     158                </div>
     159                <?php
     160        }
     161
     162                $expired_theme_suspensions = get_transient( 'wporg_themes_expired_suspension' );
     163
     164                if ( empty( $expired_theme_suspensions ) ) {
     165                        return;
     166                }
     167
     168                $themes_to_unsuspend = json_decode( $expired_theme_suspensions, true );
     169
     170                $theme_names = array_map( function( $theme_id ) {
     171                        return '<a href="' . get_edit_post_link( $theme_id ) . '" target="_blank">' . get_the_title( $theme_id ) . '</a>';
     172                }, $themes_to_unsuspend );
     173
     174                $notice = __( 'Themes that need to be reinstated: ', 'wporg-themes' ) . implode( ', ', $theme_names );
     175
     176                ?>
     177                <div class="notice notice-warning is-dismissible">
     178                        <p><?php echo wp_kses_post( $notice ); ?></p>
     179                </div>
     180                <?php
     181
     182        }
     183
     184        /**
     185         * Action ran on theme suspension
     186         *
     187         * This will set theme status to suspend, remove the theme from preview
     188         * and set the trac ticket to suspended status.
     189         */
     190        public function suspend_themes_action() {
     191                // Safety checks!
     192                $post_ids = ! empty( $_POST['post_ids'] ) ? explode( ',', $_POST['post_ids'] ) : [];
     193
     194                if ( empty( $post_ids )  ) {
     195                        wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
     196                        exit();
     197                }
     198
     199                if ( ! wp_verify_nonce( $_POST['bulk_suspend_nonce'], 'bulk_suspend_action' ) ) {
     200                        wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
     201                        exit();
     202                }
     203
     204                if ( ! isset( $_POST['bulk_suspend_nonce'] ) && $_POST['bulk_suspend_nonce'] !== 'suspend_themes' ) {
     205                        wp_safe_redirect( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page' ) );
     206                        exit();
     207                }
     208
     209                $suspension_description = $_POST['suspension-description'] ?? '';
     210
     211                if ( empty( $suspension_description ) ) {
     212                        $redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
     213                        $page_url     = add_query_arg( [ 'error' => __( 'Description cannot be empty.', 'wporg-themes' ), 'ids' => $_POST['post_ids'] ], $redirect_url );
     214
     215                        wp_safe_redirect( $page_url );
     216                        exit();
     217                }
     218
     219                $suspension_expiration_date = $_POST['suspension-date-expiration'] ?? '';
     220
     221                if ( time() > strtotime( $suspension_expiration_date ) ) {
     222                        $redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
     223                        $page_url     = add_query_arg( [ 'error' => __( 'Suspension expiration date cannot be in the past.', 'wporg-themes' ), 'ids' => $_POST['post_ids'] ], $redirect_url );
     224
     225                        wp_safe_redirect( $page_url );
     226                        exit();
     227                }
     228
     229                $current_user = wp_get_current_user()->display_name;
     230                $current_time = date( 'Y-m-d' );
     231
     232                foreach ( $post_ids as $post_id ) {
     233                        wp_update_post( array(
     234                                'ID'          => $post_id,
     235                                'post_status' => 'suspend',
     236                        ) );
     237
     238                        $theme = new WPORG_Themes_Repo_Package( $post_id );
     239
     240                        // Remove from previewer.
     241                        wporg_themes_remove_wpthemescom( $theme->post_name );
     242
     243                        $theme->ticket;
     244
     245                        $suspension_data = get_post_meta( $post_id, 'suspension_details', true );
     246
     247                        $suspension_details = [];
     248
     249                        if ( ! empty( $suspension_data ) ) {
     250                                $suspension_details = json_decode( $suspension_data, true );
     251                        }
     252
     253                        $data = [
     254                                'suspended_by'               => $current_user,
     255                                'suspended_on'               => $current_time,
     256                                'suspension_description'     => $suspension_description,
     257                                'suspension_expiration_date' => $suspension_expiration_date,
     258                        ];
     259
     260                        $suspension_details[] = $data;
     261
     262                        update_post_meta( $post_id, 'suspension_details', wp_json_encode( $suspension_details ) );
     263
     264                        // Suspend on trac.
     265                        $this->suspend_theme_on_trac( $post_id );
     266                }
     267
     268                wp_safe_redirect( esc_url( admin_url( 'edit.php?post_type=repopackage&page=suspended-packages-page&noheader=true' ) ) );
     269                exit;
     270        }
     271
     272        private function suspend_theme_on_trac( $theme_id ) {
     273                if ( ! class_exists( 'Trac' ) ) {
     274                        require_once ABSPATH . WPINC . '/class-IXR.php';
     275                        require_once ABSPATH . WPINC . '/class-wp-http-ixr-client.php';
     276                        require_once __DIR__ . '/lib/class-trac.php';
     277                }
     278
     279                // Check for tickets that were set to live previously.
     280                $trac = new Trac( 'themetracbot', THEME_TRACBOT_PASSWORD, 'https://themes.trac.wordpress.org/login/xmlrpc' );
     281
     282                $tickets = (array) $trac->ticket_query( add_query_arg( array(
     283                        'keywords'   => '~theme-' . get_post( $theme_id )->post_name,
     284                        'status'     => 'closed',
     285                        'resolution' => 'live',
     286                        'order'      => 'changetime',
     287                        'desc'       => 1,
     288                ) ) );
     289
     290                $last_live = $tickets[0];
     291
     292                $trac->ticket_update(
     293                        $last_live,
     294                        __( 'Theme is suspended for violating Theme Review Team rules', 'wporg-themes' ),
     295                        [
     296                                'action' => 'review',
     297                                'status' => 'suspended',
     298                        ],
     299                        true
     300                );
     301        }
     302
     303        /**
     304         * Generate form that is used for theme suspension
     305         *
     306         * @param string $theme_ids Comma separated list of theme ids to suspend.
     307         */
     308        private function generate_bulk_suspension_form( $theme_ids ) {
     309
     310                $error = $_REQUEST['error'] ?? '';
     311                if ( ! empty( $error ) ) {
     312                        ?>
     313                        <div class="notice error">
     314                                <?php echo '<p>' , esc_html( $error ) , '</p>' ?>
     315                        </div>
     316                        <?php
     317                }
     318
     319                if ( ! empty( $theme_ids ) ) {
     320                        $notice_text = '';
     321                        foreach ( explode( ',', $theme_ids ) as $theme_id ) {
     322                                $theme = new WPORG_Themes_Repo_Package( $theme_id );
     323
     324                                $version = $theme->version;
     325                                $ticket  = $theme->ticket;
     326
     327                                $notice_text .= '<p>' . __( 'Theme: ', 'wporg-themes' ) . esc_html( $theme->post_title ) . ', ' . esc_html( $version ) .', ' . esc_html( $ticket ) . '</p>';
     328                        }
     329                        ?>
     330                        <div class="notice">
     331                                <p><?php echo __( 'Themes to be suspended:', 'wporg-themes' ); ?></p>
     332                                <?php echo wp_kses_post( $notice_text ); ?>
     333                        </div>
     334                        <?php
     335                }
     336                ?>
     337                <form method="post" class="bulk-suspension-form" action="<?php echo esc_attr( admin_url('admin-post.php') ); ?>">
     338                        <input type="hidden" name="action" value="suspend_themes">
     339                        <input type="hidden" name="post_ids" value="<?php echo esc_attr( $theme_ids ); ?>">
     340                        <?php
     341                        wp_nonce_field( 'bulk_suspend_action', 'bulk_suspend_nonce' );
     342                         ?>
     343                        <div class="fields-wrapper">
     344                                <div class="fields-row">
     345                                        <div class="fields-column"><strong><label for="suspension-description"><?php echo __( 'Suspension reason', 'wporg-themes' ); ?></label></strong></div>
     346                                        <div class="fields-column"><strong><label for="suspension-date-expiration"><?php echo __( 'Suspension expiration date', 'wporg-themes' ); ?></label></strong></div>
     347                                </div>
     348                                <div class="fields-row">
     349                                        <div class="fields-column"><textarea name="suspension-description" id="suspension-description" cols="70" rows="10"></textarea></div>
     350                                        <div class="fields-column"><input type="date" id="suspension-date-expiration" name="suspension-date-expiration" value="" class="suspension-date-expiration-datepicker" /></div>
     351                                </div>
     352                                <div class="fields-row">
     353                                        <button type="submit" class="button button-primary"><?php echo __( 'Submit', 'wporg-themes' ); ?></button>
     354                                </div>
     355                        </div>
     356                </form>
     357                <?php
     358        }
     359}
  • new file wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-list-table.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
    - +  
     1<?php
     2/**
     3 * Administration API: WP_List_Table class
     4 *
     5 * @package WordPress
     6 * @subpackage List_Table
     7 * @since 3.1.0
     8 */
     9
     10/**
     11 * Base class for displaying a list of items in an ajaxified HTML table.
     12 *
     13 * @since 3.1.0
     14 * @access private
     15 */
     16class WPorg_Themes_List_Table {
     17
     18        /**
     19         * The current list of items.
     20         *
     21         * @since 3.1.0
     22         * @var array
     23         */
     24        public $items;
     25
     26        /**
     27         * Various information about the current table.
     28         *
     29         * @since 3.1.0
     30         * @var array
     31         */
     32        protected $_args;
     33
     34        /**
     35         * Various information needed for displaying the pagination.
     36         *
     37         * @since 3.1.0
     38         * @var array
     39         */
     40        protected $_pagination_args = array();
     41
     42        /**
     43         * The current screen.
     44         *
     45         * @since 3.1.0
     46         * @var object
     47         */
     48        protected $screen;
     49
     50        /**
     51         * Cached bulk actions.
     52         *
     53         * @since 3.1.0
     54         * @var array
     55         */
     56        private $_actions;
     57
     58        /**
     59         * Cached pagination output.
     60         *
     61         * @since 3.1.0
     62         * @var string
     63         */
     64        private $_pagination;
     65
     66        /**
     67         * The view switcher modes.
     68         *
     69         * @since 4.1.0
     70         * @var array
     71         */
     72        protected $modes = array();
     73
     74        /**
     75         * Stores the value returned by ->get_column_info().
     76         *
     77         * @since 4.1.0
     78         * @var array
     79         */
     80        protected $_column_headers;
     81
     82        /**
     83         * {@internal Missing Summary}
     84         *
     85         * @var array
     86         */
     87        protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
     88
     89        /**
     90         * {@internal Missing Summary}
     91         *
     92         * @var array
     93         */
     94        protected $compat_methods = array(
     95                'set_pagination_args',
     96                'get_views',
     97                'get_bulk_actions',
     98                'bulk_actions',
     99                'row_actions',
     100                'months_dropdown',
     101                'view_switcher',
     102                'comments_bubble',
     103                'get_items_per_page',
     104                'pagination',
     105                'get_sortable_columns',
     106                'get_column_info',
     107                'get_table_classes',
     108                'display_tablenav',
     109                'extra_tablenav',
     110                'single_row_columns',
     111        );
     112
     113        /**
     114         * Constructor.
     115         *
     116         * The child class should call this constructor from its own constructor to override
     117         * the default $args.
     118         *
     119         * @since 3.1.0
     120         *
     121         * @param array|string $args {
     122         *     Array or string of arguments.
     123         *
     124         *     @type string $plural   Plural value used for labels and the objects being listed.
     125         *                            This affects things such as CSS class-names and nonces used
     126         *                            in the list table, e.g. 'posts'. Default empty.
     127         *     @type string $singular Singular label for an object being listed, e.g. 'post'.
     128         *                            Default empty
     129         *     @type bool   $ajax     Whether the list table supports Ajax. This includes loading
     130         *                            and sorting data, for example. If true, the class will call
     131         *                            the _js_vars() method in the footer to provide variables
     132         *                            to any scripts handling Ajax events. Default false.
     133         *     @type string $screen   String containing the hook name used to determine the current
     134         *                            screen. If left null, the current screen will be automatically set.
     135         *                            Default null.
     136         * }
     137         */
     138        public function __construct( $args = array() ) {
     139                $args = wp_parse_args(
     140                        $args,
     141                        array(
     142                                'plural'   => '',
     143                                'singular' => '',
     144                                'ajax'     => false,
     145                                'screen'   => null,
     146                        )
     147                );
     148
     149                $this->screen = convert_to_screen( $args['screen'] );
     150
     151                add_filter( "manage_{$this->screen->id}_columns", array( $this, 'get_columns' ), 0 );
     152
     153                if ( ! $args['plural'] ) {
     154                        $args['plural'] = $this->screen->base;
     155                }
     156
     157                $args['plural']   = sanitize_key( $args['plural'] );
     158                $args['singular'] = sanitize_key( $args['singular'] );
     159
     160                $this->_args = $args;
     161
     162                if ( $args['ajax'] ) {
     163                        // wp_enqueue_script( 'list-table' );
     164                        add_action( 'admin_footer', array( $this, '_js_vars' ) );
     165                }
     166
     167                if ( empty( $this->modes ) ) {
     168                        $this->modes = array(
     169                                'list'    => __( 'List View' ),
     170                                'excerpt' => __( 'Excerpt View' ),
     171                        );
     172                }
     173        }
     174
     175        /**
     176         * Make private properties readable for backward compatibility.
     177         *
     178         * @since 4.0.0
     179         *
     180         * @param string $name Property to get.
     181         * @return mixed Property.
     182         */
     183        public function __get( $name ) {
     184                if ( in_array( $name, $this->compat_fields ) ) {
     185                        return $this->$name;
     186                }
     187        }
     188
     189        /**
     190         * Make private properties settable for backward compatibility.
     191         *
     192         * @since 4.0.0
     193         *
     194         * @param string $name  Property to check if set.
     195         * @param mixed  $value Property value.
     196         * @return mixed Newly-set property.
     197         */
     198        public function __set( $name, $value ) {
     199                if ( in_array( $name, $this->compat_fields ) ) {
     200                        return $this->$name = $value;
     201                }
     202        }
     203
     204        /**
     205         * Make private properties checkable for backward compatibility.
     206         *
     207         * @since 4.0.0
     208         *
     209         * @param string $name Property to check if set.
     210         * @return bool Whether the property is set.
     211         */
     212        public function __isset( $name ) {
     213                if ( in_array( $name, $this->compat_fields ) ) {
     214                        return isset( $this->$name );
     215                }
     216        }
     217
     218        /**
     219         * Make private properties un-settable for backward compatibility.
     220         *
     221         * @since 4.0.0
     222         *
     223         * @param string $name Property to unset.
     224         */
     225        public function __unset( $name ) {
     226                if ( in_array( $name, $this->compat_fields ) ) {
     227                        unset( $this->$name );
     228                }
     229        }
     230
     231        /**
     232         * Make private/protected methods readable for backward compatibility.
     233         *
     234         * @since 4.0.0
     235         *
     236         * @param string   $name      Method to call.
     237         * @param array    $arguments Arguments to pass when calling.
     238         * @return mixed|bool Return value of the callback, false otherwise.
     239         */
     240        public function __call( $name, $arguments ) {
     241                if ( in_array( $name, $this->compat_methods ) ) {
     242                        return $this->$name( ...$arguments );
     243                }
     244                return false;
     245        }
     246
     247        /**
     248         * Checks the current user's permissions
     249         *
     250         * @since 3.1.0
     251         * @abstract
     252         */
     253        public function ajax_user_can() {
     254                die( 'function WP_List_Table::ajax_user_can() must be over-ridden in a sub-class.' );
     255        }
     256
     257        /**
     258         * Prepares the list of items for displaying.
     259         *
     260         * @uses WP_List_Table::set_pagination_args()
     261         *
     262         * @since 3.1.0
     263         * @abstract
     264         */
     265        public function prepare_items() {
     266                die( 'function WP_List_Table::prepare_items() must be over-ridden in a sub-class.' );
     267        }
     268
     269        /**
     270         * An internal method that sets all the necessary pagination arguments
     271         *
     272         * @since 3.1.0
     273         *
     274         * @param array|string $args Array or string of arguments with information about the pagination.
     275         */
     276        protected function set_pagination_args( $args ) {
     277                $args = wp_parse_args(
     278                        $args,
     279                        array(
     280                                'total_items' => 0,
     281                                'total_pages' => 0,
     282                                'per_page'    => 0,
     283                        )
     284                );
     285
     286                if ( ! $args['total_pages'] && $args['per_page'] > 0 ) {
     287                        $args['total_pages'] = ceil( $args['total_items'] / $args['per_page'] );
     288                }
     289
     290                // Redirect if page number is invalid and headers are not already sent.
     291                if ( ! headers_sent() && ! wp_doing_ajax() && $args['total_pages'] > 0 && $this->get_pagenum() > $args['total_pages'] ) {
     292                        wp_redirect( add_query_arg( 'paged', $args['total_pages'] ) );
     293                        exit;
     294                }
     295
     296                $this->_pagination_args = $args;
     297        }
     298
     299        /**
     300         * Access the pagination args.
     301         *
     302         * @since 3.1.0
     303         *
     304         * @param string $key Pagination argument to retrieve. Common values include 'total_items',
     305         *                    'total_pages', 'per_page', or 'infinite_scroll'.
     306         * @return int Number of items that correspond to the given pagination argument.
     307         */
     308        public function get_pagination_arg( $key ) {
     309                if ( 'page' === $key ) {
     310                        return $this->get_pagenum();
     311                }
     312
     313                if ( isset( $this->_pagination_args[ $key ] ) ) {
     314                        return $this->_pagination_args[ $key ];
     315                }
     316        }
     317
     318        /**
     319         * Whether the table has items to display or not
     320         *
     321         * @since 3.1.0
     322         *
     323         * @return bool
     324         */
     325        public function has_items() {
     326                return ! empty( $this->items );
     327        }
     328
     329        /**
     330         * Message to be displayed when there are no items
     331         *
     332         * @since 3.1.0
     333         */
     334        public function no_items() {
     335                _e( 'No items found.' );
     336        }
     337
     338        /**
     339         * Displays the search box.
     340         *
     341         * @since 3.1.0
     342         *
     343         * @param string $text     The 'submit' button label.
     344         * @param string $input_id ID attribute value for the search input field.
     345         */
     346        public function search_box( $text, $input_id ) {
     347                if ( empty( $_REQUEST['s'] ) && ! $this->has_items() ) {
     348                        return;
     349                }
     350
     351                $input_id = $input_id . '-search-input';
     352
     353                if ( ! empty( $_REQUEST['orderby'] ) ) {
     354                        echo '<input type="hidden" name="orderby" value="' . esc_attr( $_REQUEST['orderby'] ) . '" />';
     355                }
     356                if ( ! empty( $_REQUEST['order'] ) ) {
     357                        echo '<input type="hidden" name="order" value="' . esc_attr( $_REQUEST['order'] ) . '" />';
     358                }
     359                if ( ! empty( $_REQUEST['post_mime_type'] ) ) {
     360                        echo '<input type="hidden" name="post_mime_type" value="' . esc_attr( $_REQUEST['post_mime_type'] ) . '" />';
     361                }
     362                if ( ! empty( $_REQUEST['detached'] ) ) {
     363                        echo '<input type="hidden" name="detached" value="' . esc_attr( $_REQUEST['detached'] ) . '" />';
     364                }
     365                ?>
     366<p class="search-box">
     367        <label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo $text; ?>:</label>
     368        <input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="s" value="<?php _admin_search_query(); ?>" />
     369                <?php submit_button( $text, '', '', false, array( 'id' => 'search-submit' ) ); ?>
     370</p>
     371                <?php
     372        }
     373
     374        /**
     375         * Get an associative array ( id => link ) with the list
     376         * of views available on this table.
     377         *
     378         * @since 3.1.0
     379         *
     380         * @return array
     381         */
     382        protected function get_views() {
     383                return array();
     384        }
     385
     386        /**
     387         * Display the list of views available on this table.
     388         *
     389         * @since 3.1.0
     390         */
     391        public function views() {
     392                $views = $this->get_views();
     393                /**
     394                 * Filters the list of available list table views.
     395                 *
     396                 * The dynamic portion of the hook name, `$this->screen->id`, refers
     397                 * to the ID of the current screen, usually a string.
     398                 *
     399                 * @since 3.5.0
     400                 *
     401                 * @param string[] $views An array of available list table views.
     402                 */
     403                $views = apply_filters( "views_{$this->screen->id}", $views );
     404
     405                if ( empty( $views ) ) {
     406                        return;
     407                }
     408
     409                $this->screen->render_screen_reader_content( 'heading_views' );
     410
     411                echo "<ul class='subsubsub'>\n";
     412                foreach ( $views as $class => $view ) {
     413                        $views[ $class ] = "\t<li class='$class'>$view";
     414                }
     415                echo implode( " |</li>\n", $views ) . "</li>\n";
     416                echo '</ul>';
     417        }
     418
     419        /**
     420         * Get an associative array ( option_name => option_title ) with the list
     421         * of bulk actions available on this table.
     422         *
     423         * @since 3.1.0
     424         *
     425         * @return array
     426         */
     427        protected function get_bulk_actions() {
     428                return array();
     429        }
     430
     431        /**
     432         * Display the bulk actions dropdown.
     433         *
     434         * @since 3.1.0
     435         *
     436         * @param string $which The location of the bulk actions: 'top' or 'bottom'.
     437         *                      This is designated as optional for backward compatibility.
     438         */
     439        protected function bulk_actions( $which = '' ) {
     440                if ( is_null( $this->_actions ) ) {
     441                        $this->_actions = $this->get_bulk_actions();
     442                        /**
     443                         * Filters the list table Bulk Actions drop-down.
     444                         *
     445                         * The dynamic portion of the hook name, `$this->screen->id`, refers
     446                         * to the ID of the current screen, usually a string.
     447                         *
     448                         * This filter can currently only be used to remove bulk actions.
     449                         *
     450                         * @since 3.5.0
     451                         *
     452                         * @param string[] $actions An array of the available bulk actions.
     453                         */
     454                        $this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions );  // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
     455                        $two            = '';
     456                } else {
     457                        $two = '2';
     458                }
     459
     460                if ( empty( $this->_actions ) ) {
     461                        return;
     462                }
     463
     464                echo '<label for="bulk-action-selector-' . esc_attr( $which ) . '" class="screen-reader-text">' . __( 'Select bulk action' ) . '</label>';
     465                echo '<select name="action' . $two . '" id="bulk-action-selector-' . esc_attr( $which ) . "\">\n";
     466                echo '<option value="-1">' . __( 'Bulk Actions' ) . "</option>\n";
     467
     468                foreach ( $this->_actions as $name => $title ) {
     469                        $class = 'edit' === $name ? ' class="hide-if-no-js"' : '';
     470
     471                        echo "\t" . '<option value="' . $name . '"' . $class . '>' . $title . "</option>\n";
     472                }
     473
     474                echo "</select>\n";
     475
     476                submit_button( __( 'Apply' ), 'action', '', false, array( 'id' => "doaction$two" ) );
     477                echo "\n";
     478        }
     479
     480        /**
     481         * Get the current action selected from the bulk actions dropdown.
     482         *
     483         * @since 3.1.0
     484         *
     485         * @return string|false The action name or False if no action was selected
     486         */
     487        public function current_action() {
     488                if ( isset( $_REQUEST['filter_action'] ) && ! empty( $_REQUEST['filter_action'] ) ) {
     489                        return false;
     490                }
     491
     492                if ( isset( $_REQUEST['action'] ) && -1 != $_REQUEST['action'] ) {
     493                        return $_REQUEST['action'];
     494                }
     495
     496                if ( isset( $_REQUEST['action2'] ) && -1 != $_REQUEST['action2'] ) {
     497                        return $_REQUEST['action2'];
     498                }
     499
     500                return false;
     501        }
     502
     503        /**
     504         * Generate row actions div
     505         *
     506         * @since 3.1.0
     507         *
     508         * @param string[] $actions        An array of action links.
     509         * @param bool     $always_visible Whether the actions should be always visible.
     510         * @return string
     511         */
     512        protected function row_actions( $actions, $always_visible = false ) {
     513                $action_count = count( $actions );
     514                $i            = 0;
     515
     516                if ( ! $action_count ) {
     517                        return '';
     518                }
     519
     520                $out = '<div class="' . ( $always_visible ? 'row-actions visible' : 'row-actions' ) . '">';
     521                foreach ( $actions as $action => $link ) {
     522                        ++$i;
     523                        ( $i == $action_count ) ? $sep = '' : $sep = ' | ';
     524                        $out                          .= "<span class='$action'>$link$sep</span>";
     525                }
     526                $out .= '</div>';
     527
     528                $out .= '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>';
     529
     530                return $out;
     531        }
     532
     533        /**
     534         * Display a monthly dropdown for filtering items
     535         *
     536         * @since 3.1.0
     537         *
     538         * @global wpdb      $wpdb      WordPress database abstraction object.
     539         * @global WP_Locale $wp_locale WordPress date and time locale object.
     540         *
     541         * @param string $post_type
     542         */
     543        protected function months_dropdown( $post_type ) {
     544                global $wpdb, $wp_locale;
     545
     546                /**
     547                 * Filters whether to remove the 'Months' drop-down from the post list table.
     548                 *
     549                 * @since 4.2.0
     550                 *
     551                 * @param bool   $disable   Whether to disable the drop-down. Default false.
     552                 * @param string $post_type The post type.
     553                 */
     554                if ( apply_filters( 'disable_months_dropdown', false, $post_type ) ) {
     555                        return;
     556                }
     557
     558                $extra_checks = "AND post_status != 'auto-draft'";
     559                if ( ! isset( $_GET['post_status'] ) || 'trash' !== $_GET['post_status'] ) {
     560                        $extra_checks .= " AND post_status != 'trash'";
     561                } elseif ( isset( $_GET['post_status'] ) ) {
     562                        $extra_checks = $wpdb->prepare( ' AND post_status = %s', $_GET['post_status'] );
     563                }
     564
     565                $months = $wpdb->get_results(
     566                        $wpdb->prepare(
     567                                "
     568                        SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
     569                        FROM $wpdb->posts
     570                        WHERE post_type = %s
     571                        $extra_checks
     572                        ORDER BY post_date DESC
     573                ",
     574                                $post_type
     575                        )
     576                );
     577
     578                /**
     579                 * Filters the 'Months' drop-down results.
     580                 *
     581                 * @since 3.7.0
     582                 *
     583                 * @param object $months    The months drop-down query results.
     584                 * @param string $post_type The post type.
     585                 */
     586                $months = apply_filters( 'months_dropdown_results', $months, $post_type );
     587
     588                $month_count = count( $months );
     589
     590                if ( ! $month_count || ( 1 == $month_count && 0 == $months[0]->month ) ) {
     591                        return;
     592                }
     593
     594                $m = isset( $_GET['m'] ) ? (int) $_GET['m'] : 0;
     595                ?>
     596                <label for="filter-by-date" class="screen-reader-text"><?php _e( 'Filter by date' ); ?></label>
     597                <select name="m" id="filter-by-date">
     598                        <option<?php selected( $m, 0 ); ?> value="0"><?php _e( 'All dates' ); ?></option>
     599                <?php
     600                foreach ( $months as $arc_row ) {
     601                        if ( 0 == $arc_row->year ) {
     602                                continue;
     603                        }
     604
     605                        $month = zeroise( $arc_row->month, 2 );
     606                        $year  = $arc_row->year;
     607
     608                        printf(
     609                                "<option %s value='%s'>%s</option>\n",
     610                                selected( $m, $year . $month, false ),
     611                                esc_attr( $arc_row->year . $month ),
     612                                /* translators: 1: Month name, 2: 4-digit year. */
     613                                sprintf( __( '%1$s %2$d' ), $wp_locale->get_month( $month ), $year )
     614                        );
     615                }
     616                ?>
     617                </select>
     618                <?php
     619        }
     620
     621        /**
     622         * Display a view switcher
     623         *
     624         * @since 3.1.0
     625         *
     626         * @param string $current_mode
     627         */
     628        protected function view_switcher( $current_mode ) {
     629                ?>
     630                <input type="hidden" name="mode" value="<?php echo esc_attr( $current_mode ); ?>" />
     631                <div class="view-switch">
     632                <?php
     633                foreach ( $this->modes as $mode => $title ) {
     634                        $classes = array( 'view-' . $mode );
     635                        if ( $current_mode === $mode ) {
     636                                $classes[] = 'current';
     637                        }
     638                        printf(
     639                                "<a href='%s' class='%s' id='view-switch-$mode'><span class='screen-reader-text'>%s</span></a>\n",
     640                                esc_url( add_query_arg( 'mode', $mode ) ),
     641                                implode( ' ', $classes ),
     642                                $title
     643                        );
     644                }
     645                ?>
     646                </div>
     647                <?php
     648        }
     649
     650        /**
     651         * Display a comment count bubble
     652         *
     653         * @since 3.1.0
     654         *
     655         * @param int $post_id          The post ID.
     656         * @param int $pending_comments Number of pending comments.
     657         */
     658        protected function comments_bubble( $post_id, $pending_comments ) {
     659                $approved_comments = get_comments_number();
     660
     661                $approved_comments_number = number_format_i18n( $approved_comments );
     662                $pending_comments_number  = number_format_i18n( $pending_comments );
     663
     664                $approved_only_phrase = sprintf(
     665                        /* translators: %s: Number of comments. */
     666                        _n( '%s comment', '%s comments', $approved_comments ),
     667                        $approved_comments_number
     668                );
     669
     670                $approved_phrase = sprintf(
     671                        /* translators: %s: Number of comments. */
     672                        _n( '%s approved comment', '%s approved comments', $approved_comments ),
     673                        $approved_comments_number
     674                );
     675
     676                $pending_phrase = sprintf(
     677                        /* translators: %s: Number of comments. */
     678                        _n( '%s pending comment', '%s pending comments', $pending_comments ),
     679                        $pending_comments_number
     680                );
     681
     682                // No comments at all.
     683                if ( ! $approved_comments && ! $pending_comments ) {
     684                        printf(
     685                                '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">%s</span>',
     686                                __( 'No comments' )
     687                        );
     688                        // Approved comments have different display depending on some conditions.
     689                } elseif ( $approved_comments ) {
     690                        printf(
     691                                '<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>',
     692                                esc_url(
     693                                        add_query_arg(
     694                                                array(
     695                                                        'p'              => $post_id,
     696                                                        'comment_status' => 'approved',
     697                                                ),
     698                                                admin_url( 'edit-comments.php' )
     699                                        )
     700                                ),
     701                                $approved_comments_number,
     702                                $pending_comments ? $approved_phrase : $approved_only_phrase
     703                        );
     704                } else {
     705                        printf(
     706                                '<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>',
     707                                $approved_comments_number,
     708                                $pending_comments ? __( 'No approved comments' ) : __( 'No comments' )
     709                        );
     710                }
     711
     712                if ( $pending_comments ) {
     713                        printf(
     714                                '<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>',
     715                                esc_url(
     716                                        add_query_arg(
     717                                                array(
     718                                                        'p'              => $post_id,
     719                                                        'comment_status' => 'moderated',
     720                                                ),
     721                                                admin_url( 'edit-comments.php' )
     722                                        )
     723                                ),
     724                                $pending_comments_number,
     725                                $pending_phrase
     726                        );
     727                } else {
     728                        printf(
     729                                '<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>',
     730                                $pending_comments_number,
     731                                $approved_comments ? __( 'No pending comments' ) : __( 'No comments' )
     732                        );
     733                }
     734        }
     735
     736        /**
     737         * Get the current page number
     738         *
     739         * @since 3.1.0
     740         *
     741         * @return int
     742         */
     743        public function get_pagenum() {
     744                $pagenum = isset( $_REQUEST['paged'] ) ? absint( $_REQUEST['paged'] ) : 0;
     745
     746                if ( isset( $this->_pagination_args['total_pages'] ) && $pagenum > $this->_pagination_args['total_pages'] ) {
     747                        $pagenum = $this->_pagination_args['total_pages'];
     748                }
     749
     750                return max( 1, $pagenum );
     751        }
     752
     753        /**
     754         * Get number of items to display on a single page
     755         *
     756         * @since 3.1.0
     757         *
     758         * @param string $option
     759         * @param int    $default
     760         * @return int
     761         */
     762        protected function get_items_per_page( $option, $default = 20 ) {
     763                $per_page = (int) get_user_option( $option );
     764                if ( empty( $per_page ) || $per_page < 1 ) {
     765                        $per_page = $default;
     766                }
     767
     768                /**
     769                 * Filters the number of items to be displayed on each page of the list table.
     770                 *
     771                 * The dynamic hook name, $option, refers to the `per_page` option depending
     772                 * on the type of list table in use. Possible values include: 'edit_comments_per_page',
     773                 * 'sites_network_per_page', 'site_themes_network_per_page', 'themes_network_per_page',
     774                 * 'users_network_per_page', 'edit_post_per_page', 'edit_page_per_page',
     775                 * 'edit_{$post_type}_per_page', etc.
     776                 *
     777                 * @since 2.9.0
     778                 *
     779                 * @param int $per_page Number of items to be displayed. Default 20.
     780                 */
     781                return (int) apply_filters( "{$option}", $per_page );
     782        }
     783
     784        /**
     785         * Display the pagination.
     786         *
     787         * @since 3.1.0
     788         *
     789         * @param string $which
     790         */
     791        protected function pagination( $which ) {
     792                if ( empty( $this->_pagination_args ) ) {
     793                        return;
     794                }
     795
     796                $total_items     = $this->_pagination_args['total_items'];
     797                $total_pages     = $this->_pagination_args['total_pages'];
     798                $infinite_scroll = false;
     799                if ( isset( $this->_pagination_args['infinite_scroll'] ) ) {
     800                        $infinite_scroll = $this->_pagination_args['infinite_scroll'];
     801                }
     802
     803                if ( 'top' === $which && $total_pages > 1 ) {
     804                        $this->screen->render_screen_reader_content( 'heading_pagination' );
     805                }
     806
     807                $output = '<span class="displaying-num">' . sprintf(
     808                        /* translators: %s: Number of items. */
     809                        _n( '%s item', '%s items', $total_items ),
     810                        number_format_i18n( $total_items )
     811                ) . '</span>';
     812
     813                $current              = $this->get_pagenum();
     814                $removable_query_args = wp_removable_query_args();
     815
     816                $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
     817
     818                $current_url = remove_query_arg( $removable_query_args, $current_url );
     819
     820                $page_links = array();
     821
     822                $total_pages_before = '<span class="paging-input">';
     823                $total_pages_after  = '</span></span>';
     824
     825                $disable_first = false;
     826                $disable_last  = false;
     827                $disable_prev  = false;
     828                $disable_next  = false;
     829
     830                if ( $current == 1 ) {
     831                        $disable_first = true;
     832                        $disable_prev  = true;
     833                }
     834                if ( $current == 2 ) {
     835                        $disable_first = true;
     836                }
     837                if ( $current == $total_pages ) {
     838                        $disable_last = true;
     839                        $disable_next = true;
     840                }
     841                if ( $current == $total_pages - 1 ) {
     842                        $disable_last = true;
     843                }
     844
     845                if ( $disable_first ) {
     846                        $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&laquo;</span>';
     847                } else {
     848                        $page_links[] = sprintf(
     849                                "<a class='first-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
     850                                esc_url( remove_query_arg( 'paged', $current_url ) ),
     851                                __( 'First page' ),
     852                                '&laquo;'
     853                        );
     854                }
     855
     856                if ( $disable_prev ) {
     857                        $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&lsaquo;</span>';
     858                } else {
     859                        $page_links[] = sprintf(
     860                                "<a class='prev-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
     861                                esc_url( add_query_arg( 'paged', max( 1, $current - 1 ), $current_url ) ),
     862                                __( 'Previous page' ),
     863                                '&lsaquo;'
     864                        );
     865                }
     866
     867                if ( 'bottom' === $which ) {
     868                        $html_current_page  = $current;
     869                        $total_pages_before = '<span class="screen-reader-text">' . __( 'Current Page' ) . '</span><span id="table-paging" class="paging-input"><span class="tablenav-paging-text">';
     870                } else {
     871                        $html_current_page = sprintf(
     872                                "%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'>",
     873                                '<label for="current-page-selector" class="screen-reader-text">' . __( 'Current Page' ) . '</label>',
     874                                $current,
     875                                strlen( $total_pages )
     876                        );
     877                }
     878                $html_total_pages = sprintf( "<span class='total-pages'>%s</span>", number_format_i18n( $total_pages ) );
     879                $page_links[]     = $total_pages_before . sprintf(
     880                        /* translators: 1: Current page, 2: Total pages. */
     881                        _x( '%1$s of %2$s', 'paging' ),
     882                        $html_current_page,
     883                        $html_total_pages
     884                ) . $total_pages_after;
     885
     886                if ( $disable_next ) {
     887                        $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&rsaquo;</span>';
     888                } else {
     889                        $page_links[] = sprintf(
     890                                "<a class='next-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
     891                                esc_url( add_query_arg( 'paged', min( $total_pages, $current + 1 ), $current_url ) ),
     892                                __( 'Next page' ),
     893                                '&rsaquo;'
     894                        );
     895                }
     896
     897                if ( $disable_last ) {
     898                        $page_links[] = '<span class="tablenav-pages-navspan button disabled" aria-hidden="true">&raquo;</span>';
     899                } else {
     900                        $page_links[] = sprintf(
     901                                "<a class='last-page button' href='%s'><span class='screen-reader-text'>%s</span><span aria-hidden='true'>%s</span></a>",
     902                                esc_url( add_query_arg( 'paged', $total_pages, $current_url ) ),
     903                                __( 'Last page' ),
     904                                '&raquo;'
     905                        );
     906                }
     907
     908                $pagination_links_class = 'pagination-links';
     909                if ( ! empty( $infinite_scroll ) ) {
     910                        $pagination_links_class .= ' hide-if-js';
     911                }
     912                $output .= "\n<span class='$pagination_links_class'>" . join( "\n", $page_links ) . '</span>';
     913
     914                if ( $total_pages ) {
     915                        $page_class = $total_pages < 2 ? ' one-page' : '';
     916                } else {
     917                        $page_class = ' no-pages';
     918                }
     919                $this->_pagination = "<div class='tablenav-pages{$page_class}'>$output</div>";
     920
     921                echo $this->_pagination;
     922        }
     923
     924        /**
     925         * Get a list of columns. The format is:
     926         * 'internal-name' => 'Title'
     927         *
     928         * @since 3.1.0
     929         * @abstract
     930         *
     931         * @return array
     932         */
     933        public function get_columns() {
     934                die( 'function WP_List_Table::get_columns() must be over-ridden in a sub-class.' );
     935        }
     936
     937        /**
     938         * Get a list of sortable columns. The format is:
     939         * 'internal-name' => 'orderby'
     940         * or
     941         * 'internal-name' => array( 'orderby', true )
     942         *
     943         * The second format will make the initial sorting order be descending
     944         *
     945         * @since 3.1.0
     946         *
     947         * @return array
     948         */
     949        protected function get_sortable_columns() {
     950                return array();
     951        }
     952
     953        /**
     954         * Gets the name of the default primary column.
     955         *
     956         * @since 4.3.0
     957         *
     958         * @return string Name of the default primary column, in this case, an empty string.
     959         */
     960        protected function get_default_primary_column_name() {
     961                $columns = $this->get_columns();
     962                $column  = '';
     963
     964                if ( empty( $columns ) ) {
     965                        return $column;
     966                }
     967
     968                // We need a primary defined so responsive views show something,
     969                // so let's fall back to the first non-checkbox column.
     970                foreach ( $columns as $col => $column_name ) {
     971                        if ( 'cb' === $col ) {
     972                                continue;
     973                        }
     974
     975                        $column = $col;
     976                        break;
     977                }
     978
     979                return $column;
     980        }
     981
     982        /**
     983         * Public wrapper for WP_List_Table::get_default_primary_column_name().
     984         *
     985         * @since 4.4.0
     986         *
     987         * @return string Name of the default primary column.
     988         */
     989        public function get_primary_column() {
     990                return $this->get_primary_column_name();
     991        }
     992
     993        /**
     994         * Gets the name of the primary column.
     995         *
     996         * @since 4.3.0
     997         *
     998         * @return string The name of the primary column.
     999         */
     1000        protected function get_primary_column_name() {
     1001                $columns = get_column_headers( $this->screen );
     1002                $default = $this->get_default_primary_column_name();
     1003
     1004                // If the primary column doesn't exist fall back to the
     1005                // first non-checkbox column.
     1006                if ( ! isset( $columns[ $default ] ) ) {
     1007                        $default = self::get_default_primary_column_name();
     1008                }
     1009
     1010                /**
     1011                 * Filters the name of the primary column for the current list table.
     1012                 *
     1013                 * @since 4.3.0
     1014                 *
     1015                 * @param string $default Column name default for the specific list table, e.g. 'name'.
     1016                 * @param string $context Screen ID for specific list table, e.g. 'plugins'.
     1017                 */
     1018                $column = apply_filters( 'list_table_primary_column', $default, $this->screen->id );
     1019
     1020                if ( empty( $column ) || ! isset( $columns[ $column ] ) ) {
     1021                        $column = $default;
     1022                }
     1023
     1024                return $column;
     1025        }
     1026
     1027        /**
     1028         * Get a list of all, hidden and sortable columns, with filter applied
     1029         *
     1030         * @since 3.1.0
     1031         *
     1032         * @return array
     1033         */
     1034        protected function get_column_info() {
     1035                // $_column_headers is already set / cached
     1036                if ( isset( $this->_column_headers ) && is_array( $this->_column_headers ) ) {
     1037                        // Back-compat for list tables that have been manually setting $_column_headers for horse reasons.
     1038                        // In 4.3, we added a fourth argument for primary column.
     1039                        $column_headers = array( array(), array(), array(), $this->get_primary_column_name() );
     1040                        foreach ( $this->_column_headers as $key => $value ) {
     1041                                $column_headers[ $key ] = $value;
     1042                        }
     1043
     1044                        return $column_headers;
     1045                }
     1046
     1047                $columns = get_column_headers( $this->screen );
     1048                $hidden  = get_hidden_columns( $this->screen );
     1049
     1050                $sortable_columns = $this->get_sortable_columns();
     1051                /**
     1052                 * Filters the list table sortable columns for a specific screen.
     1053                 *
     1054                 * The dynamic portion of the hook name, `$this->screen->id`, refers
     1055                 * to the ID of the current screen, usually a string.
     1056                 *
     1057                 * @since 3.5.0
     1058                 *
     1059                 * @param array $sortable_columns An array of sortable columns.
     1060                 */
     1061                $_sortable = apply_filters( "manage_{$this->screen->id}_sortable_columns", $sortable_columns );
     1062
     1063                $sortable = array();
     1064                foreach ( $_sortable as $id => $data ) {
     1065                        if ( empty( $data ) ) {
     1066                                continue;
     1067                        }
     1068
     1069                        $data = (array) $data;
     1070                        if ( ! isset( $data[1] ) ) {
     1071                                $data[1] = false;
     1072                        }
     1073
     1074                        $sortable[ $id ] = $data;
     1075                }
     1076
     1077                $primary               = $this->get_primary_column_name();
     1078                $this->_column_headers = array( $columns, $hidden, $sortable, $primary );
     1079
     1080                return $this->_column_headers;
     1081        }
     1082
     1083        /**
     1084         * Return number of visible columns
     1085         *
     1086         * @since 3.1.0
     1087         *
     1088         * @return int
     1089         */
     1090        public function get_column_count() {
     1091                list ( $columns, $hidden ) = $this->get_column_info();
     1092                $hidden                    = array_intersect( array_keys( $columns ), array_filter( $hidden ) );
     1093                return count( $columns ) - count( $hidden );
     1094        }
     1095
     1096        /**
     1097         * Print column headers, accounting for hidden and sortable columns.
     1098         *
     1099         * @since 3.1.0
     1100         *
     1101         * @staticvar int $cb_counter
     1102         *
     1103         * @param bool $with_id Whether to set the id attribute or not
     1104         */
     1105        public function print_column_headers( $with_id = true ) {
     1106                list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
     1107
     1108                $current_url = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] );
     1109                $current_url = remove_query_arg( 'paged', $current_url );
     1110
     1111                if ( isset( $_GET['orderby'] ) ) {
     1112                        $current_orderby = $_GET['orderby'];
     1113                } else {
     1114                        $current_orderby = '';
     1115                }
     1116
     1117                if ( isset( $_GET['order'] ) && 'desc' === $_GET['order'] ) {
     1118                        $current_order = 'desc';
     1119                } else {
     1120                        $current_order = 'asc';
     1121                }
     1122
     1123                if ( ! empty( $columns['cb'] ) ) {
     1124                        static $cb_counter = 1;
     1125                        $columns['cb']     = '<label class="screen-reader-text" for="cb-select-all-' . $cb_counter . '">' . __( 'Select All' ) . '</label>'
     1126                                . '<input id="cb-select-all-' . $cb_counter . '" type="checkbox" />';
     1127                        $cb_counter++;
     1128                }
     1129
     1130                foreach ( $columns as $column_key => $column_display_name ) {
     1131                        $class = array( 'manage-column', "column-$column_key" );
     1132
     1133                        if ( in_array( $column_key, $hidden ) ) {
     1134                                $class[] = 'hidden';
     1135                        }
     1136
     1137                        if ( 'cb' === $column_key ) {
     1138                                $class[] = 'check-column';
     1139                        } elseif ( in_array( $column_key, array( 'posts', 'comments', 'links' ) ) ) {
     1140                                $class[] = 'num';
     1141                        }
     1142
     1143                        if ( $column_key === $primary ) {
     1144                                $class[] = 'column-primary';
     1145                        }
     1146
     1147                        if ( isset( $sortable[ $column_key ] ) ) {
     1148                                list( $orderby, $desc_first ) = $sortable[ $column_key ];
     1149
     1150                                if ( $current_orderby === $orderby ) {
     1151                                        $order   = 'asc' === $current_order ? 'desc' : 'asc';
     1152                                        $class[] = 'sorted';
     1153                                        $class[] = $current_order;
     1154                                } else {
     1155                                        $order   = $desc_first ? 'desc' : 'asc';
     1156                                        $class[] = 'sortable';
     1157                                        $class[] = $desc_first ? 'asc' : 'desc';
     1158                                }
     1159
     1160                                $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>';
     1161                        }
     1162
     1163                        $tag   = ( 'cb' === $column_key ) ? 'td' : 'th';
     1164                        $scope = ( 'th' === $tag ) ? 'scope="col"' : '';
     1165                        $id    = $with_id ? "id='$column_key'" : '';
     1166
     1167                        if ( ! empty( $class ) ) {
     1168                                $class = "class='" . join( ' ', $class ) . "'";
     1169                        }
     1170
     1171                        echo "<$tag $scope $id $class>$column_display_name</$tag>";
     1172                }
     1173        }
     1174
     1175        /**
     1176         * Displays the table.
     1177         *
     1178         * @since 3.1.0
     1179         */
     1180        public function display() {
     1181                $singular = $this->_args['singular'];
     1182
     1183                $this->display_tablenav( 'top' );
     1184
     1185                $this->screen->render_screen_reader_content( 'heading_list' );
     1186                ?>
     1187<table class="wp-list-table <?php echo implode( ' ', $this->get_table_classes() ); ?>">
     1188        <thead>
     1189        <tr>
     1190                <?php $this->print_column_headers(); ?>
     1191        </tr>
     1192        </thead>
     1193
     1194        <tbody id="the-list"
     1195                <?php
     1196                if ( $singular ) {
     1197                        echo " data-wp-lists='list:$singular'";
     1198                }
     1199                ?>
     1200                >
     1201                <?php $this->display_rows_or_placeholder(); ?>
     1202        </tbody>
     1203
     1204        <tfoot>
     1205        <tr>
     1206                <?php $this->print_column_headers( false ); ?>
     1207        </tr>
     1208        </tfoot>
     1209
     1210</table>
     1211                <?php
     1212                $this->display_tablenav( 'bottom' );
     1213        }
     1214
     1215        /**
     1216         * Get a list of CSS classes for the WP_List_Table table tag.
     1217         *
     1218         * @since 3.1.0
     1219         *
     1220         * @return array List of CSS classes for the table tag.
     1221         */
     1222        protected function get_table_classes() {
     1223                return array( 'widefat', 'fixed', 'striped', $this->_args['plural'] );
     1224        }
     1225
     1226        /**
     1227         * Generate the table navigation above or below the table
     1228         *
     1229         * @since 3.1.0
     1230         * @param string $which
     1231         */
     1232        protected function display_tablenav( $which ) {
     1233                if ( 'top' === $which ) {
     1234                        wp_nonce_field( 'bulk-' . $this->_args['plural'] );
     1235                }
     1236                ?>
     1237        <div class="tablenav <?php echo esc_attr( $which ); ?>">
     1238
     1239                <?php if ( $this->has_items() ) : ?>
     1240                <div class="alignleft actions bulkactions">
     1241                        <?php $this->bulk_actions( $which ); ?>
     1242                </div>
     1243                        <?php
     1244                endif;
     1245                $this->extra_tablenav( $which );
     1246                $this->pagination( $which );
     1247                ?>
     1248
     1249                <br class="clear" />
     1250        </div>
     1251                <?php
     1252        }
     1253
     1254        /**
     1255         * Extra controls to be displayed between bulk actions and pagination
     1256         *
     1257         * @since 3.1.0
     1258         *
     1259         * @param string $which
     1260         */
     1261        protected function extra_tablenav( $which ) {}
     1262
     1263        /**
     1264         * Generate the tbody element for the list table.
     1265         *
     1266         * @since 3.1.0
     1267         */
     1268        public function display_rows_or_placeholder() {
     1269                if ( $this->has_items() ) {
     1270                        $this->display_rows();
     1271                } else {
     1272                        echo '<tr class="no-items"><td class="colspanchange" colspan="' . $this->get_column_count() . '">';
     1273                        $this->no_items();
     1274                        echo '</td></tr>';
     1275                }
     1276        }
     1277
     1278        /**
     1279         * Generate the table rows
     1280         *
     1281         * @since 3.1.0
     1282         */
     1283        public function display_rows() {
     1284                foreach ( $this->items as $item ) {
     1285                        $this->single_row( $item );
     1286                }
     1287        }
     1288
     1289        /**
     1290         * Generates content for a single row of the table
     1291         *
     1292         * @since 3.1.0
     1293         *
     1294         * @param object $item The current item
     1295         */
     1296        public function single_row( $item ) {
     1297                echo '<tr>';
     1298                $this->single_row_columns( $item );
     1299                echo '</tr>';
     1300        }
     1301
     1302        /**
     1303         * @param object $item
     1304         * @param string $column_name
     1305         */
     1306        protected function column_default( $item, $column_name ) {}
     1307
     1308        /**
     1309         * @param object $item
     1310         */
     1311        protected function column_cb( $item ) {}
     1312
     1313        /**
     1314         * Generates the columns for a single row of the table
     1315         *
     1316         * @since 3.1.0
     1317         *
     1318         * @param object $item The current item
     1319         */
     1320        protected function single_row_columns( $item ) {
     1321                list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
     1322
     1323                foreach ( $columns as $column_name => $column_display_name ) {
     1324                        $classes = "$column_name column-$column_name";
     1325                        if ( $primary === $column_name ) {
     1326                                $classes .= ' has-row-actions column-primary';
     1327                        }
     1328
     1329                        if ( in_array( $column_name, $hidden ) ) {
     1330                                $classes .= ' hidden';
     1331                        }
     1332
     1333                        // Comments column uses HTML in the display name with screen reader text.
     1334                        // Instead of using esc_attr(), we strip tags to get closer to a user-friendly string.
     1335                        $data = 'data-colname="' . wp_strip_all_tags( $column_display_name ) . '"';
     1336
     1337                        $attributes = "class='$classes' $data";
     1338
     1339                        if ( 'cb' === $column_name ) {
     1340                                echo '<th scope="row" class="check-column">';
     1341                                echo $this->column_cb( $item );
     1342                                echo '</th>';
     1343                        } elseif ( method_exists( $this, '_column_' . $column_name ) ) {
     1344                                echo call_user_func(
     1345                                        array( $this, '_column_' . $column_name ),
     1346                                        $item,
     1347                                        $classes,
     1348                                        $data,
     1349                                        $primary
     1350                                );
     1351                        } elseif ( method_exists( $this, 'column_' . $column_name ) ) {
     1352                                echo "<td $attributes>";
     1353                                echo call_user_func( array( $this, 'column_' . $column_name ), $item );
     1354                                echo $this->handle_row_actions( $item, $column_name, $primary );
     1355                                echo '</td>';
     1356                        } else {
     1357                                echo "<td $attributes>";
     1358                                echo $this->column_default( $item, $column_name );
     1359                                echo $this->handle_row_actions( $item, $column_name, $primary );
     1360                                echo '</td>';
     1361                        }
     1362                }
     1363        }
     1364
     1365        /**
     1366         * Generates and display row actions links for the list table.
     1367         *
     1368         * @since 4.3.0
     1369         *
     1370         * @param object $item        The item being acted upon.
     1371         * @param string $column_name Current column name.
     1372         * @param string $primary     Primary column name.
     1373         * @return string The row actions HTML, or an empty string if the current column is the primary column.
     1374         */
     1375        protected function handle_row_actions( $item, $column_name, $primary ) {
     1376                return $column_name === $primary ? '<button type="button" class="toggle-row"><span class="screen-reader-text">' . __( 'Show more details' ) . '</span></button>' : '';
     1377        }
     1378
     1379        /**
     1380         * Handle an incoming ajax request (called from admin-ajax.php)
     1381         *
     1382         * @since 3.1.0
     1383         */
     1384        public function ajax_response() {
     1385                $this->prepare_items();
     1386
     1387                ob_start();
     1388                if ( ! empty( $_REQUEST['no_placeholder'] ) ) {
     1389                        $this->display_rows();
     1390                } else {
     1391                        $this->display_rows_or_placeholder();
     1392                }
     1393
     1394                $rows = ob_get_clean();
     1395
     1396                $response = array( 'rows' => $rows );
     1397
     1398                if ( isset( $this->_pagination_args['total_items'] ) ) {
     1399                        $response['total_items_i18n'] = sprintf(
     1400                                /* translators: Number of items. */
     1401                                _n( '%s item', '%s items', $this->_pagination_args['total_items'] ),
     1402                                number_format_i18n( $this->_pagination_args['total_items'] )
     1403                        );
     1404                }
     1405                if ( isset( $this->_pagination_args['total_pages'] ) ) {
     1406                        $response['total_pages']      = $this->_pagination_args['total_pages'];
     1407                        $response['total_pages_i18n'] = number_format_i18n( $this->_pagination_args['total_pages'] );
     1408                }
     1409
     1410                die( wp_json_encode( $response ) );
     1411        }
     1412
     1413        /**
     1414         * Send required variables to JavaScript land
     1415         */
     1416        public function _js_vars() {
     1417                $args = array(
     1418                        'class'  => get_class( $this ),
     1419                        'screen' => array(
     1420                                'id'   => $this->screen->id,
     1421                                'base' => $this->screen->base,
     1422                        ),
     1423                );
     1424
     1425                printf( "<script type='text/javascript'>list_args = %s;</script>\n", wp_json_encode( $args ) );
     1426        }
     1427}
  • new file wordpress.org/public_html/wp-content/plugins/theme-directory/inc/class-wporg-themes-suspended-list-table.php

    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
    - +  
     1<?php
     2// If WP_List_Table doesn't exist, fall back to the included one.
     3if ( ! class_exists( 'WPorg_Themes_List_Table' ) ) {
     4        include __DIR__ . '/class-wporg-themes-list-table.php';
     5}
     6
     7/**
     8 * Class WPorg_Themes_Suspended_List_Table
     9 *
     10 * Extends the extracted WP_List_Table table from core
     11 */
     12class WPorg_Themes_Suspended_List_Table extends WPorg_Themes_List_Table {
     13        /**
     14         * WPorg_Themes_Suspended_List_Table constructor.
     15         */
     16        public function __construct() {
     17                parent::__construct(
     18                        array(
     19                                'singular' => __( 'Suspended theme', 'wporg-themes' ),
     20                                'plural'   => __( 'Suspended themes', 'wporg-themes' ),
     21                                'ajax'     => false
     22                        )
     23                );
     24        }
     25
     26        /**
     27         * Prepare the items for the table to process
     28         *
     29         * @return void
     30         */
     31        public function prepare_items() {
     32
     33                $columns  = $this->get_columns();
     34                $hidden   = $this->get_hidden_columns();
     35                $sortable = $this->get_sortable_columns();
     36                $data     = $this->table_data();
     37
     38
     39                usort( $data, array( &$this, 'sort_data' ) );
     40
     41                $per_page = get_option( 'posts_per_page' );
     42
     43                $current_page = $this->get_pagenum();
     44                $total_items = count( $data );
     45
     46                $this->set_pagination_args( [
     47                        'total_items' => $total_items,
     48                        'per_page'    => $per_page
     49                ] );
     50
     51                $data = array_slice( $data, ( ( $current_page-1 ) * $per_page ), $per_page );
     52                $this->_column_headers = [ $columns, $hidden, $sortable ];
     53                $this->items = $data;
     54
     55                $this->process_bulk_action();
     56        }
     57
     58        /**
     59         * Markup for checkbox column
     60         *
     61         * @param object $item Current item in the list.
     62         *
     63         * @return string|void
     64         */
     65        function column_cb( $item ) {
     66                return sprintf(
     67                        '<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" />',
     68                        $item['ID'],
     69                        $item['title']
     70                );
     71        }
     72
     73        /**
     74         * Display custom bulk actions
     75         *
     76         * @return array
     77         */
     78        public function get_bulk_actions() {
     79                return [
     80                        'reinstate' => __( 'Reinstate', 'wporg-theme' ),
     81                ];
     82        }
     83
     84        /**
     85         * Process custom bulk actions
     86         */
     87        public function process_bulk_action() {
     88                // Security check.
     89                if ( isset( $_POST['_wpnonce'] ) && ! empty( $_POST['_wpnonce'] ) ) {
     90
     91                        $nonce  = filter_input( INPUT_POST, '_wpnonce', FILTER_SANITIZE_STRING );
     92                        $action = 'bulk-' . $this->_args['plural'];
     93
     94                        if ( ! wp_verify_nonce( $nonce, $action ) ) {
     95                                wp_die( 'Nope! Security check failed!' );
     96                        }
     97                }
     98
     99                if ( 'reinstate' === $this->current_action() ) {
     100                        // Reinstate the themes.
     101                        $themes_to_reinstate = $_POST['post'];
     102
     103                        foreach ( $themes_to_reinstate as $theme_id ) {
     104                                wp_update_post( array(
     105                                        'ID'          => (int) $theme_id,
     106                                        'post_status' => 'draft',
     107                                ) );
     108
     109                                /*
     110                                 * Mark it as reinstated, so the post date doesn't get overwritten when it's
     111                                 * published again.
     112                                 */
     113                                add_post_meta( (int) $theme_id, '_wporg_themes_reinstated', true );
     114                        }
     115
     116                        $redirect_url = admin_url('edit.php?post_type=repopackage');
     117                        $page_url     = add_query_arg( [ 'reinstated_notice' => __( 'Themes reinstated.', 'wporg-themes' ) ], $redirect_url );
     118
     119                        wp_safe_redirect( $page_url );
     120                        exit;
     121                }
     122        }
     123
     124        /**
     125         * Override the parent columns method. Defines the columns to use in your listing table
     126         *
     127         * @return array
     128         */
     129        public function get_columns() {
     130                return [
     131                        'cb'                                     => '<input type="checkbox" />',
     132                        'title'                  => __( 'Theme title', 'wporg-theme' ),
     133                        'ticket'                 => __( 'Ticket ID', 'wporg-theme' ),
     134                        'author'                 => __( 'Author', 'wporg-theme' ),
     135                        'suspended-by'           => __( 'Suspended by', 'wporg-theme' ),
     136                        'suspension-reason'      => __( 'Reason', 'wporg-theme' ),
     137                        'date-of-suspension'     => __( 'Date', 'wporg-theme' ),
     138                        'suspension-expiry-date' => __( 'Expiry', 'wporg-theme' ),
     139                ];
     140        }
     141
     142/**
     143         * Define which columns are hidden
     144         *
     145         * @return array
     146         */
     147        public function get_hidden_columns() {
     148                return [];
     149        }
     150
     151        /**
     152         * Define the sortable columns
     153         *
     154         * @return array
     155         */
     156        public function get_sortable_columns() {
     157                return [
     158                        'title'                  => [ 'title', false ],
     159                        'date-of-suspension'     => [ 'date-of-suspension', false ],
     160                        'suspension-expiry-date' => [ 'suspension-expiry-date', false ],
     161                ];
     162        }
     163
     164         /**
     165         * Define what data to show on each column of the table
     166         *
     167         * @param  array $item         Data
     168         * @param  string $column_name Current column name
     169         *
     170         * @return mixed
     171         */
     172        public function column_default( $item, $column_name ) {
     173                switch( $column_name ) {
     174                        case 'ID':
     175                        case 'title':
     176                        case 'ticket':
     177                        case 'author':
     178                        case 'suspended-by':
     179                        case 'suspension-reason':
     180                        case 'date-of-suspension':
     181                        case 'suspension-expiry-date':
     182                                return $item[ $column_name ];
     183                        default:
     184                                return print_r( $item, true ) ;
     185                }
     186        }
     187
     188        /**
     189         * Get the table data
     190         *
     191         * @return array
     192         */
     193        private function table_data() {
     194                $suspended_themes = new WP_Query([
     195                        'post_type'   => 'repopackage',
     196                        'post_status' => 'suspend',
     197                ]);
     198
     199                $suspended_themes_total = $suspended_themes->posts;
     200
     201                return array_map( static function( $theme ) {
     202                        $theme_id  = $theme->ID;
     203                        $author_id = $theme->post_author;
     204
     205                        $theme = new WPORG_Themes_Repo_Package( $theme_id );
     206
     207                        $suspensions = get_post_meta( $theme_id, 'suspension_details', true );
     208
     209                        $suspended_by = '';
     210                        $suspension_description = '';
     211                        $suspended_on = '';
     212                        $suspension_expiration_date = '';
     213
     214                        if ( ! empty( $suspensions ) ) {
     215                                $suspension_details = json_decode( $suspensions, true );
     216                                $last_suspension    = end( $suspension_details );
     217
     218                                $suspended_by               = $last_suspension['suspended_by'];
     219                                $suspension_description     = $last_suspension['suspension_description'];
     220                                $suspended_on               = $last_suspension['suspended_on'];
     221                                $suspension_expiration_date = $last_suspension['suspension_expiration_date'];
     222                        }
     223
     224
     225                        return [
     226                                'ID'                     => $theme_id,
     227                                'title'                  => get_the_title( $theme_id ),
     228                                'ticket'                 => $theme->ticket,
     229                                'author'                 => get_the_author_meta( 'display_name', $author_id ),
     230                                'suspended-by'           => $suspended_by,
     231                                'suspension-reason'      => $suspension_description,
     232                                'date-of-suspension'     => $suspended_on,
     233                                'suspension-expiry-date' => $suspension_expiration_date,
     234                        ];
     235                }, $suspended_themes_total );
     236        }
     237
     238        /**
     239         * Allows you to sort the data by the variables set in the $_GET
     240         *
     241         * @return mixed
     242         */
     243        private function sort_data( $a, $b ) {
     244                // Set defaults
     245                $orderby = 'title';
     246                $order   = 'asc';
     247
     248                // If orderby is set, use this as the sort column
     249                if ( ! empty( $_GET['orderby'] ) ) {
     250                        $orderby = $_GET['orderby'];
     251                }
     252
     253                // If order is set use this as the order
     254                if ( !empty( $_GET['order'] ) ) {
     255                        $order = $_GET['order'];
     256                }
     257
     258                $result = strcmp( $a[$orderby], $b[$orderby] );
     259
     260                if ( $order === 'asc' ) {
     261                        return $result;
     262                }
     263
     264                return -$result;
     265        }
     266}
  • wordpress.org/public_html/wp-content/plugins/theme-directory/jobs/class-trac-sync.php

    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
    class Trac_Sync { 
    3131                        'status'     => 'closed',
    3232                        'resolution' => 'not-approved',
    3333                ],
     34                'suspended'  => [
     35                        'status'     => 'suspended',
     36                        'resolution' => 'not-approved',
     37                ],
    3438        ];
    3539
    3640        /**
  • wordpress.org/public_html/wp-content/plugins/theme-directory/theme-directory.php

    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
    include __DIR__ . '/jobs/class-manager.php'; 
    3434include __DIR__ . '/jobs/class-trac-sync.php';
    3535new WordPressdotorg\Theme_Directory\Jobs\Manager();
    3636
     37// Load suspended pages.
     38include __DIR__ . '/class-wporg-themes-suspended-repo-packages.php';
     39
     40( new WPorg_Themes_Suspended_Repo_Packages() )->register();
     41
    3742/**
    3843 * Things to change on activation.
    3944 */
    function wporg_themes_get_current_url( $path_only = false ) { 
    12971302
    12981303        return $link;
    12991304}
     1305
     1306/**
     1307 * Add suspend bulk action
     1308 */
     1309add_action( 'current_screen', 'wporg_themes_bulk_hooks' );
     1310
     1311/**
     1312 * Hooks to current_screen hook, so that we have the WP_Screen defined
     1313 */
     1314function wporg_themes_bulk_hooks() {
     1315        add_filter( 'bulk_actions-edit-repopackage', 'wporg_themes_add_bulk_suspend_option' );
     1316        add_filter( 'handle_bulk_actions-edit-repopackage', 'wporg_themes_suspend_bulk_action_handler', 10, 3 );
     1317        add_action( 'admin_notices', 'wporg_themes_suspend_bulk_action_admin_notice' );
     1318}
     1319
     1320/**
     1321 * Add a new bulk action
     1322 *
     1323 * Add the suspend themes action
     1324 *
     1325 * @param  array $bulk_actions Array of bulk actions.
     1326 * @return array               Updated array of bulk actions.
     1327 */
     1328function wporg_themes_add_bulk_suspend_option( $bulk_actions ) {
     1329        $bulk_actions['suspend_themes'] = __( 'Suspend themes', 'wporg-themes' );
     1330
     1331        return $bulk_actions;
     1332}
     1333
     1334/**
     1335 * Add the action handler for the suspend themes action
     1336 *
     1337 * @param  string $redirect_to The redirect URL.
     1338 * @param  string $doaction    The action being taken.
     1339 * @param  array  $post_ids    The post IDs to take the action on.
     1340 * @return string              Redirection URL after the action.
     1341 */
     1342function wporg_themes_suspend_bulk_action_handler( $redirect_to, $doaction, $post_ids ) {
     1343        if ( $doaction !== 'suspend_themes' ) {
     1344                return $redirect_to;
     1345        }
     1346
     1347        $themes_to_suspend = array_filter( $post_ids, static function( $post_id ) {
     1348                if ( get_post_status( $post_id ) !== 'suspend' ) {
     1349                        return $post_id;
     1350                }
     1351        } );
     1352
     1353        $number_of_themes_to_suspend = count( $themes_to_suspend );
     1354
     1355        $redirect_url = admin_url('edit.php?post_type=repopackage&page=suspended-packages-bulk-message-page');
     1356        $page_url     = add_query_arg( [ 'ids' => implode( ',', $themes_to_suspend ), 'count' => $number_of_themes_to_suspend ], $redirect_url );
     1357
     1358        $redirect_to = add_query_arg( 'bulk_suspend_themes', $number_of_themes_to_suspend, $page_url );
     1359
     1360        return $redirect_to;
     1361}
     1362
     1363/**
     1364 * The admin notice after the suspend themes action happened.
     1365 */
     1366function wporg_themes_suspend_bulk_action_admin_notice() {
     1367
     1368        if ( ! empty( $_REQUEST['bulk_suspend_themes'] ) ) {
     1369                $suspended_themes_count = (int) $_REQUEST['bulk_suspend_themes'];
     1370
     1371                printf( '<div id="message" class="updated notice is-dismissible"><p>' .
     1372                        _n( '%s theme will be suspended.',
     1373                                '%s themes will be suspended.',
     1374                                $suspended_themes_count,
     1375                                'wporg-themes'
     1376                        ) . '</p><button type="button" class="notice-dismiss"><span class="screen-reader-text">' . __(' Dismiss this notice.', 'wporg-themes' ) . '</span></button></div>',
     1377                        $suspended_themes_count );
     1378        }
     1379}