• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Yoast / wordpress-seo / fe4d74ae81b231d2986738b4e4648c24370c79ea

26 Feb 2025 02:54PM UTC coverage: 45.177% (-5.5%) from 50.712%
fe4d74ae81b231d2986738b4e4648c24370c79ea

push

github

enricobattocchi
Drop compatibility with PHP 7.2 and 7.3

15990 of 35394 relevant lines covered (45.18%)

4.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/admin/class-bulk-editor-list-table.php
1
<?php
2
/**
3
 * WPSEO plugin file.
4
 *
5
 * @package WPSEO\Admin\Bulk Editor
6
 * @since   1.5.0
7
 */
8

9
/**
10
 * Implements table for bulk editing.
11
 */
12
class WPSEO_Bulk_List_Table extends WP_List_Table {
13

14
        /**
15
         * The nonce that was passed with the request.
16
         *
17
         * @var string
18
         */
19
        private $nonce;
20

21
        /**
22
         * Array of post types for which the current user has `edit_others_posts` capabilities.
23
         *
24
         * @var array
25
         */
26
        private $all_posts;
27

28
        /**
29
         * Array of post types for which the current user has `edit_posts` capabilities, but not `edit_others_posts`.
30
         *
31
         * @var array
32
         */
33
        private $own_posts;
34

35
        /**
36
         * Saves all the metadata into this array.
37
         *
38
         * @var array
39
         */
40
        protected $meta_data = [];
41

42
        /**
43
         * The current requested page_url.
44
         *
45
         * @var string
46
         */
47
        private $request_url = '';
48

49
        /**
50
         * The current page (depending on $_GET['paged']) if current tab is for current page_type, else it will be 1.
51
         *
52
         * @var int
53
         */
54
        private $current_page;
55

56
        /**
57
         * The current post filter, if is used (depending on $_GET['post_type_filter']).
58
         *
59
         * @var string
60
         */
61
        private $current_filter;
62

63
        /**
64
         * The current post status, if is used (depending on $_GET['post_status']).
65
         *
66
         * @var string
67
         */
68
        private $current_status;
69

70
        /**
71
         * The current sorting, if used (depending on $_GET['order'] and $_GET['orderby']).
72
         *
73
         * @var string
74
         */
75
        private $current_order;
76

77
        /**
78
         * The page_type for current class instance (for example: title / description).
79
         *
80
         * @var string
81
         */
82
        protected $page_type;
83

84
        /**
85
         * Based on the page_type ($this->page_type) there will be constructed an url part, for subpages and
86
         * navigation.
87
         *
88
         * @var string
89
         */
90
        protected $page_url;
91

92
        /**
93
         * The settings which will be used in the __construct.
94
         *
95
         * @var array
96
         */
97
        protected $settings;
98

99
        /**
100
         * Holds the pagination config.
101
         *
102
         * @var array
103
         */
104
        protected $pagination = [];
105

106
        /**
107
         * Holds the sanitized data from the user input.
108
         *
109
         * @var array
110
         */
111
        protected $input_fields = [];
112

113
        /**
114
         * The field in the database where meta field is saved.
115
         *
116
         * Should be set in the child class.
117
         *
118
         * @var string
119
         */
120
        protected $target_db_field = '';
121

122
        /**
123
         * Class constructor.
124
         *
125
         * @param array $args The arguments.
126
         */
127
        public function __construct( $args = [] ) {
×
128
                parent::__construct( $this->settings );
×
129

130
                $args = wp_parse_args(
×
131
                        $args,
×
132
                        [
×
133
                                'nonce'        => '',
×
134
                                'input_fields' => [],
×
135
                        ]
×
136
                );
×
137

138
                $this->input_fields = $args['input_fields'];
×
139
                if ( isset( $_SERVER['REQUEST_URI'] ) ) {
×
140
                        $this->request_url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
×
141
                }
142

143
                $this->current_page   = ( ! empty( $this->input_fields['paged'] ) ) ? $this->input_fields['paged'] : 1;
×
144
                $this->current_filter = ( ! empty( $this->input_fields['post_type_filter'] ) ) ? $this->input_fields['post_type_filter'] : 1;
×
145
                $this->current_status = ( ! empty( $this->input_fields['post_status'] ) ) ? $this->input_fields['post_status'] : 1;
×
146
                $this->current_order  = [
×
147
                        'order'   => ( ! empty( $this->input_fields['order'] ) ) ? $this->input_fields['order'] : 'asc',
×
148
                        'orderby' => ( ! empty( $this->input_fields['orderby'] ) ) ? $this->input_fields['orderby'] : 'post_title',
×
149
                ];
×
150

151
                $this->nonce    = $args['nonce'];
×
152
                $this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}";
×
153

154
                $this->populate_editable_post_types();
×
155
        }
156

157
        /**
158
         * Prepares the data and renders the page.
159
         *
160
         * @return void
161
         */
162
        public function show_page() {
×
163
                $this->prepare_page_navigation();
×
164
                $this->prepare_items();
×
165

166
                $this->views();
×
167
                $this->display();
×
168
        }
169

170
        /**
171
         * Used in the constructor to build a reference list of post types the current user can edit.
172
         *
173
         * @return void
174
         */
175
        protected function populate_editable_post_types() {
×
176
                $post_types = get_post_types(
×
177
                        [
×
178
                                'public'              => true,
×
179
                                'exclude_from_search' => false,
×
180
                        ],
×
181
                        'object'
×
182
                );
×
183

184
                $this->all_posts = [];
×
185
                $this->own_posts = [];
×
186

187
                if ( is_array( $post_types ) && $post_types !== [] ) {
×
188
                        foreach ( $post_types as $post_type ) {
×
189
                                if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
×
190
                                        continue;
×
191
                                }
192

193
                                if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
×
194
                                        $this->all_posts[] = esc_sql( $post_type->name );
×
195
                                }
196
                                else {
197
                                        $this->own_posts[] = esc_sql( $post_type->name );
×
198
                                }
199
                        }
200
                }
201
        }
202

203
        /**
204
         * Will show the navigation for the table like page navigation and page filter.
205
         *
206
         * @param string $which Table nav location (such as top).
207
         *
208
         * @return void
209
         */
210
        public function display_tablenav( $which ) {
×
211
                // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
212
                $post_status      = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : '';
×
213
                $order_by         = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';
×
214
                $order            = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : '';
×
215
                $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
×
216
                // phpcs:enable WordPress.Security.NonceVerification.Recommended;
217
                ?>
218
                <div class="tablenav <?php echo esc_attr( $which ); ?>">
×
219

220
                        <?php if ( $which === 'top' ) { ?>
×
221
                        <form id="posts-filter" action="" method="get">
×
222
                                <input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>"/>
×
223
                                <input type="hidden" name="page" value="wpseo_tools"/>
224
                                <input type="hidden" name="tool" value="bulk-editor"/>
225
                                <input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>"/>
×
226
                                <input type="hidden" name="orderby"
227
                                        value="<?php echo esc_attr( $order_by ); ?>"/>
×
228
                                <input type="hidden" name="order"
229
                                        value="<?php echo esc_attr( $order ); ?>"/>
×
230
                                <input type="hidden" name="post_type_filter"
231
                                        value="<?php echo esc_attr( $post_type_filter ); ?>"/>
×
232
                                <?php if ( ! empty( $post_status ) ) { ?>
×
233
                                        <input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>"/>
×
234
                                <?php } ?>
235
                                <?php } ?>
×
236

237
                                <?php
×
238
                                $this->extra_tablenav( $which );
×
239
                                $this->pagination( $which );
×
240
                                ?>
241

242
                                <br class="clear"/>
×
243
                                <?php if ( $which === 'top' ) { ?>
×
244
                        </form>
×
245
                <?php } ?>
×
246
                </div>
×
247

248
                <?php
×
249
        }
250

251
        /**
252
         * This function builds the base sql subquery used in this class.
253
         *
254
         * This function takes into account the post types in which the current user can
255
         * edit all posts, and the ones the current user can only edit his/her own.
256
         *
257
         * @return string The subquery, which should always be used in $wpdb->prepare(),
258
         *                passing the current user_id in as the first parameter.
259
         */
260
        public function get_base_subquery() {
×
261
                global $wpdb;
×
262

263
                $all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'";
×
264
                $own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'";
×
265

266
                $post_author = esc_sql( (int) get_current_user_id() );
×
267

268
                $subquery = "(
×
269
                                SELECT *
270
                                FROM {$wpdb->posts}
×
271
                                WHERE post_type IN ({$all_posts_string})
×
272
                                UNION ALL
273
                                SELECT *
274
                                FROM {$wpdb->posts}
×
275
                                WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author}
×
276
                        ) sub_base";
×
277

278
                return $subquery;
×
279
        }
280

281
        /**
282
         * Gets the views.
283
         *
284
         * @return array The views.
285
         */
286
        public function get_views() {
×
287
                global $wpdb;
×
288

289
                $status_links = [];
×
290

291
                $states   = get_post_stati( [ 'show_in_admin_all_list' => true ] );
×
292
                $subquery = $this->get_base_subquery();
×
293

294
                $total_posts = $wpdb->get_var(
×
295
                        $wpdb->prepare(
×
296
                                "SELECT COUNT(ID) FROM {$subquery}
×
297
                                        WHERE post_status IN ("
×
298
                                                . implode( ', ', array_fill( 0, count( $states ), '%s' ) )
×
299
                                        . ')',
×
300
                                $states
×
301
                        )
×
302
                );
×
303

304
                $post_status             = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : '';
×
305
                $current_link_attributes = empty( $post_status ) ? ' class="current" aria-current="page"' : '';
×
306
                $localized_text          = sprintf(
×
307
                        /* translators: %s expands to the number of posts in localized format. */
308
                        _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ),
×
309
                        number_format_i18n( $total_posts )
×
310
                );
×
311

312
                $status_links['all'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>';
×
313

314
                $post_stati = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );
×
315
                if ( is_array( $post_stati ) && $post_stati !== [] ) {
×
316
                        foreach ( $post_stati as $status ) {
×
317

318
                                $status_name = esc_sql( $status->name );
×
319

320
                                $total = (int) $wpdb->get_var(
×
321
                                        $wpdb->prepare(
×
322
                                                "
×
323
                                                                SELECT COUNT(ID) FROM {$subquery}
×
324
                                                                WHERE post_status = %s
325
                                                        ",
×
326
                                                $status_name
×
327
                                        )
×
328
                                );
×
329

330
                                if ( $total === 0 ) {
×
331
                                        continue;
×
332
                                }
333

334
                                $current_link_attributes = '';
×
335
                                if ( $status_name === $post_status ) {
×
336
                                        $current_link_attributes = ' class="current" aria-current="page"';
×
337
                                }
338

339
                                $status_links[ $status_name ] = '<a href="' . esc_url( add_query_arg( [ 'post_status' => $status_name ], admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor' . $this->page_url ) ) ) . '"' . $current_link_attributes . '>' . sprintf( translate_nooped_plural( $status->label_count, $total ), number_format_i18n( $total ) ) . '</a>';
×
340
                        }
341
                }
342
                unset( $post_stati, $status, $status_name, $total, $current_link_attributes );
×
343

344
                $trashed_posts = $wpdb->get_var(
×
345
                        "SELECT COUNT(ID) FROM {$subquery}
×
346
                                WHERE post_status IN ('trash')
347
                        "
×
348
                );
×
349

350
                $current_link_attributes = '';
×
351
                if ( $post_status === 'trash' ) {
×
352
                        $current_link_attributes = 'class="current" aria-current="page"';
×
353
                }
354

355
                $localized_text = sprintf(
×
356
                        /* translators: %s expands to the number of trashed posts in localized format. */
357
                        _nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ),
×
358
                        number_format_i18n( $trashed_posts )
×
359
                );
×
360

361
                $status_links['trash'] = '<a href="' . esc_url( admin_url( 'admin.php?page=wpseo_tools&tool=bulk-editor&post_status=trash' . $this->page_url ) ) . '"' . $current_link_attributes . '>' . $localized_text . '</a>';
×
362

363
                return $status_links;
×
364
        }
365

366
        /**
367
         * Outputs extra table navigation.
368
         *
369
         * @param string $which Table nav location (such as top).
370
         *
371
         * @return void
372
         */
373
        public function extra_tablenav( $which ) {
×
374

375
                if ( $which === 'top' ) {
×
376
                        $post_types = get_post_types(
×
377
                                [
×
378
                                        'public'              => true,
×
379
                                        'exclude_from_search' => false,
×
380
                                ]
×
381
                        );
×
382

383
                        $instance_type = esc_attr( $this->page_type );
×
384

385
                        if ( is_array( $post_types ) && $post_types !== [] ) {
×
386
                                global $wpdb;
×
387

388
                                echo '<div class="alignleft actions">';
×
389

390
                                $post_types = esc_sql( $post_types );
×
391
                                $post_types = "'" . implode( "', '", $post_types ) . "'";
×
392

393
                                $states          = get_post_stati( [ 'show_in_admin_all_list' => true ] );
×
394
                                $states['trash'] = 'trash';
×
395

396
                                $subquery = $this->get_base_subquery();
×
397

398
                                $post_types = $wpdb->get_results(
×
399
                                        $wpdb->prepare(
×
400
                                                "SELECT DISTINCT post_type FROM {$subquery}
×
401
                                                        WHERE post_status IN ("
×
402
                                                                . implode( ', ', array_fill( 0, count( $states ), '%s' ) )
×
403
                                                        . ') ORDER BY post_type ASC',
×
404
                                                $states
×
405
                                        )
×
406
                                );
×
407

408
                                $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
×
409
                                $selected         = ( ! empty( $post_type_filter ) ) ? $post_type_filter : '-1';
×
410

411
                                $options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>';
×
412

413
                                if ( is_array( $post_types ) && $post_types !== [] ) {
×
414
                                        foreach ( $post_types as $post_type ) {
×
415
                                                $obj      = get_post_type_object( $post_type->post_type );
×
416
                                                $options .= sprintf(
×
417
                                                        '<option value="%2$s" %3$s>%1$s</option>',
×
418
                                                        esc_html( $obj->labels->name ),
×
419
                                                        esc_attr( $post_type->post_type ),
×
420
                                                        selected( $selected, $post_type->post_type, false )
×
421
                                                );
×
422
                                        }
423
                                }
424

425
                                printf(
×
426
                                        '<label for="%1$s" class="screen-reader-text">%2$s</label>',
×
427
                                        esc_attr( 'post-type-filter-' . $instance_type ),
×
428
                                        /* translators: Hidden accessibility text. */
429
                                        esc_html__( 'Filter by content type', 'wordpress-seo' )
×
430
                                );
×
431
                                printf(
×
432
                                        '<select name="post_type_filter" id="%2$s">%1$s</select>',
×
433
                                        // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $options is properly escaped above.
434
                                        $options,
×
435
                                        esc_attr( 'post-type-filter-' . $instance_type )
×
436
                                );
×
437

438
                                submit_button( esc_html__( 'Filter', 'wordpress-seo' ), 'button', false, false, [ 'id' => 'post-query-submit' ] );
×
439
                                echo '</div>';
×
440
                        }
441
                }
442
        }
443

444
        /**
445
         * Gets a list of sortable columns.
446
         *
447
         * The format is: 'internal-name' => array( 'orderby', bool ).
448
         *
449
         * @return array
450
         */
451
        public function get_sortable_columns() {
×
452
                return [
×
453
                        'col_page_title' => [ 'post_title', true ],
×
454
                        'col_post_type'  => [ 'post_type', false ],
×
455
                        'col_post_date'  => [ 'post_date', false ],
×
456
                ];
×
457
        }
458

459
        /**
460
         * Sets the correct pagenumber and pageurl for the navigation.
461
         *
462
         * @return void
463
         */
464
        public function prepare_page_navigation() {
×
465

466
                $request_url = $this->request_url . $this->page_url;
×
467

468
                $current_page   = $this->current_page;
×
469
                $current_filter = $this->current_filter;
×
470
                $current_status = $this->current_status;
×
471
                $current_order  = $this->current_order;
×
472

473
                /*
474
                 * If current type doesn't compare with objects page_type, then we have to unset
475
                 * some vars in the requested url (which will be used for internal table urls).
476
                 */
477
                if ( isset( $this->input_fields['type'] ) && $this->input_fields['type'] !== $this->page_type ) {
×
478
                        $request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below.
×
479
                        $request_url = remove_query_arg( 'post_type_filter', $request_url );
×
480
                        $request_url = remove_query_arg( 'post_status', $request_url );
×
481
                        $request_url = remove_query_arg( 'orderby', $request_url );
×
482
                        $request_url = remove_query_arg( 'order', $request_url );
×
483
                        $request_url = add_query_arg( 'pages', 1, $request_url );
×
484

485
                        $current_page   = 1;
×
486
                        $current_filter = '-1';
×
487
                        $current_status = '';
×
488
                        $current_order  = [
×
489
                                'orderby' => 'post_title',
×
490
                                'order'   => 'asc',
×
491
                        ];
×
492
                }
493

494
                $_SERVER['REQUEST_URI'] = $request_url;
×
495

496
                $_GET['paged']                = $current_page;
×
497
                $_REQUEST['paged']            = $current_page;
×
498
                $_REQUEST['post_type_filter'] = $current_filter;
×
499
                $_GET['post_type_filter']     = $current_filter;
×
500
                $_GET['post_status']          = $current_status;
×
501
                $_GET['orderby']              = $current_order['orderby'];
×
502
                $_GET['order']                = $current_order['order'];
×
503
        }
504

505
        /**
506
         * Preparing the requested pagerows and setting the needed variables.
507
         *
508
         * @return void
509
         */
510
        public function prepare_items() {
×
511

512
                $post_type_clause = $this->get_post_type_clause();
×
513
                $all_states       = $this->get_all_states();
×
514
                $subquery         = $this->get_base_subquery();
×
515

516
                // Setting the column headers.
517
                $this->set_column_headers();
×
518

519
                // Count the total number of needed items and setting pagination given $total_items.
520
                $total_items = $this->count_items( $subquery, $all_states, $post_type_clause );
×
521
                $this->set_pagination( $total_items );
×
522

523
                // Getting items given $query.
524
                $query = $this->parse_item_query( $subquery, $all_states, $post_type_clause );
×
525
                $this->get_items( $query );
×
526

527
                // Get the metadata for the current items ($this->items).
528
                $this->get_meta_data();
×
529
        }
530

531
        /**
532
         * Getting the columns for first row.
533
         *
534
         * @return array
535
         */
536
        public function get_columns() {
×
537
                return $this->merge_columns();
×
538
        }
539

540
        /**
541
         * Setting the column headers.
542
         *
543
         * @return void
544
         */
545
        protected function set_column_headers() {
×
546
                $columns               = $this->get_columns();
×
547
                $hidden                = [];
×
548
                $sortable              = $this->get_sortable_columns();
×
549
                $this->_column_headers = [ $columns, $hidden, $sortable ];
×
550
        }
551

552
        /**
553
         * Counting total items.
554
         *
555
         * @param string $subquery         SQL FROM part.
556
         * @param string $all_states       SQL IN part.
557
         * @param string $post_type_clause SQL post type part.
558
         *
559
         * @return mixed
560
         */
561
        protected function count_items( $subquery, $all_states, $post_type_clause ) {
×
562
                global $wpdb;
×
563

564
                return (int) $wpdb->get_var(
×
565
                        "SELECT COUNT(ID) FROM {$subquery}
×
566
                                WHERE post_status IN ({$all_states})
×
567
                                        {$post_type_clause}
×
568
                        "
×
569
                );
×
570
        }
571

572
        /**
573
         * Getting the post_type_clause filter.
574
         *
575
         * @return string
576
         */
577
        protected function get_post_type_clause() {
×
578
                // Filter Block.
579
                $post_type_clause = '';
×
580
                $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
×
581

582
                if ( ! empty( $post_type_filter ) && get_post_type_object( $post_type_filter ) ) {
×
583
                        $post_types       = esc_sql( $post_type_filter );
×
584
                        $post_type_clause = "AND post_type IN ('{$post_types}')";
×
585
                }
586

587
                return $post_type_clause;
×
588
        }
589

590
        /**
591
         * Setting the pagination.
592
         *
593
         * Total items is the number of all visible items.
594
         *
595
         * @param int $total_items Total items counts.
596
         *
597
         * @return void
598
         */
599
        protected function set_pagination( $total_items ) {
×
600
                // Calculate items per page.
601
                $per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 );
×
602
                $paged    = isset( $_GET['paged'] ) && is_string( $_GET['paged'] ) ? esc_sql( sanitize_text_field( wp_unslash( $_GET['paged'] ) ) ) : '';
×
603

604
                if ( empty( $paged ) || ! is_numeric( $paged ) ) {
×
605
                        $paged = 1;
×
606
                }
607
                else {
608
                        $paged = (int) $paged;
×
609
                }
610

611
                if ( $paged <= 0 ) {
×
612
                        $paged = 1;
×
613
                }
614

615
                $this->set_pagination_args(
×
616
                        [
×
617
                                'total_items' => $total_items,
×
618
                                'total_pages' => ceil( $total_items / $per_page ),
×
619
                                'per_page'    => $per_page,
×
620
                        ]
×
621
                );
×
622

623
                $this->pagination = [
×
624
                        'per_page' => $per_page,
×
625
                        'offset'   => ( ( $paged - 1 ) * $per_page ),
×
626
                ];
×
627
        }
628

629
        /**
630
         * Parse the query to get items from database.
631
         *
632
         * Based on given parameters there will be parse a query which will get all the pages/posts and other post_types
633
         * from the database.
634
         *
635
         * @param string $subquery         SQL FROM part.
636
         * @param string $all_states       SQL IN part.
637
         * @param string $post_type_clause SQL post type part.
638
         *
639
         * @return string
640
         */
641
        protected function parse_item_query( $subquery, $all_states, $post_type_clause ) {
×
642
                // Order By block.
643
                $orderby = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';
×
644

645
                $orderby = ! empty( $orderby ) ? esc_sql( $orderby ) : 'post_title';
×
646
                $orderby = $this->sanitize_orderby( $orderby );
×
647

648
                // Order clause.
649
                $order = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : '';
×
650
                $order = ! empty( $order ) ? esc_sql( strtoupper( $order ) ) : 'ASC';
×
651
                $order = $this->sanitize_order( $order );
×
652

653
                // Get all needed results.
654
                $query = "
×
655
                        SELECT ID, post_title, post_type, post_status, post_modified, post_date
656
                                FROM {$subquery}
×
657
                                WHERE post_status IN ({$all_states}) $post_type_clause
×
658
                                ORDER BY {$orderby} {$order}
×
659
                                LIMIT %d,%d
660
                        ";
×
661

662
                return $query;
×
663
        }
664

665
        /**
666
         * Heavily restricts the possible columns by which a user can order the table
667
         * in the bulk editor, thereby preventing a possible CSRF vulnerability.
668
         *
669
         * @param string $orderby The column by which we want to order.
670
         *
671
         * @return string
672
         */
673
        protected function sanitize_orderby( $orderby ) {
×
674
                $valid_column_names = [
×
675
                        'post_title',
×
676
                        'post_type',
×
677
                        'post_date',
×
678
                ];
×
679

680
                if ( in_array( $orderby, $valid_column_names, true ) ) {
×
681
                        return $orderby;
×
682
                }
683

684
                return 'post_title';
×
685
        }
686

687
        /**
688
         * Makes sure the order clause is always ASC or DESC for the bulk editor table,
689
         * thereby preventing a possible CSRF vulnerability.
690
         *
691
         * @param string $order Whether we want to sort ascending or descending.
692
         *
693
         * @return string SQL order string (ASC, DESC).
694
         */
695
        protected function sanitize_order( $order ) {
×
696
                if ( in_array( strtoupper( $order ), [ 'ASC', 'DESC' ], true ) ) {
×
697
                        return $order;
×
698
                }
699

700
                return 'ASC';
×
701
        }
702

703
        /**
704
         * Getting all the items.
705
         *
706
         * @param string $query SQL query to use.
707
         *
708
         * @return void
709
         */
710
        protected function get_items( $query ) {
×
711
                global $wpdb;
×
712

713
                $this->items = $wpdb->get_results(
×
714
                        $wpdb->prepare(
×
715
                                $query,
×
716
                                $this->pagination['offset'],
×
717
                                $this->pagination['per_page']
×
718
                        )
×
719
                );
×
720
        }
721

722
        /**
723
         * Getting all the states.
724
         *
725
         * @return string
726
         */
727
        protected function get_all_states() {
×
728
                global $wpdb;
×
729

730
                $states          = get_post_stati( [ 'show_in_admin_all_list' => true ] );
×
731
                $states['trash'] = 'trash';
×
732

733
                if ( ! empty( $this->input_fields['post_status'] ) ) {
×
734
                        $requested_state = $this->input_fields['post_status'];
×
735
                        if ( in_array( $requested_state, $states, true ) ) {
×
736
                                $states = [ $requested_state ];
×
737
                        }
738

739
                        if ( $requested_state !== 'trash' ) {
×
740
                                unset( $states['trash'] );
×
741
                        }
742
                }
743

744
                return $wpdb->prepare(
×
745
                        implode( ', ', array_fill( 0, count( $states ), '%s' ) ),
×
746
                        $states
×
747
                );
×
748
        }
749

750
        /**
751
         * Based on $this->items and the defined columns, the table rows will be displayed.
752
         *
753
         * @return void
754
         */
755
        public function display_rows() {
×
756

757
                $records = $this->items;
×
758

759
                list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
×
760

761
                if ( ( is_array( $records ) && $records !== [] ) && ( is_array( $columns ) && $columns !== [] ) ) {
×
762

763
                        foreach ( $records as $record ) {
×
764

765
                                echo '<tr id="', esc_attr( 'record_' . $record->ID ), '">';
×
766

767
                                foreach ( $columns as $column_name => $column_display_name ) {
×
768

769
                                        $classes = '';
×
770
                                        if ( $primary === $column_name ) {
×
771
                                                $classes .= ' has-row-actions column-primary';
×
772
                                        }
773

774
                                        $attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name );
×
775

776
                                        $column_value = $this->parse_column( $column_name, $record );
×
777

778
                                        if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) {
×
779
                                                $column_value = $this->parse_page_specific_column( $column_name, $record, $attributes );
×
780
                                        }
781

782
                                        if ( ! empty( $column_value ) ) {
×
783
                                                printf( '<td %2$s>%1$s</td>', $column_value, $attributes );
×
784
                                        }
785
                                }
786

787
                                echo '</tr>';
×
788
                        }
789
                }
790
        }
791

792
        /**
793
         * Getting the attributes for each table cell.
794
         *
795
         * @param string $column_name         Column name string.
796
         * @param array  $hidden              Set of hidden columns.
797
         * @param string $classes             Additional CSS classes.
798
         * @param string $column_display_name Column display name string.
799
         *
800
         * @return string
801
         */
802
        protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) {
×
803

804
                $attributes = '';
×
805
                $class      = [ $column_name, "column-$column_name$classes" ];
×
806

807
                if ( in_array( $column_name, $hidden, true ) ) {
×
808
                        $class[] = 'hidden';
×
809
                }
810

811
                if ( ! empty( $class ) ) {
×
812
                        $attributes = 'class="' . esc_attr( implode( ' ', $class ) ) . '"';
×
813
                }
814

815
                $attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"';
×
816

817
                return $attributes;
×
818
        }
819

820
        /**
821
         * Parsing the title.
822
         *
823
         * @param WP_Post $rec Post object.
824
         *
825
         * @return string
826
         */
827
        protected function parse_page_title_column( $rec ) {
×
828

829
                $title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title;
×
830

831
                $return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) );
×
832

833
                $post_type_object = get_post_type_object( $rec->post_type );
×
834
                $can_edit_post    = current_user_can( $post_type_object->cap->edit_post, $rec->ID );
×
835

836
                $actions = [];
×
837

838
                if ( $can_edit_post && $rec->post_status !== 'trash' ) {
×
839
                        $actions['edit'] = sprintf(
×
840
                                '<a href="%s" aria-label="%s">%s</a>',
×
841
                                esc_url( get_edit_post_link( $rec->ID, true ) ),
×
842
                                /* translators: Hidden accessibility text; %s: post title. */
843
                                esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
×
844
                                __( 'Edit', 'wordpress-seo' )
×
845
                        );
×
846
                }
847

848
                if ( $post_type_object->public ) {
×
849
                        if ( in_array( $rec->post_status, [ 'pending', 'draft', 'future' ], true ) ) {
×
850
                                if ( $can_edit_post ) {
×
851
                                        $actions['view'] = sprintf(
×
852
                                                '<a href="%s" aria-label="%s">%s</a>',
×
853
                                                esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ),
×
854
                                                /* translators: Hidden accessibility text; %s: post title. */
855
                                                esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
×
856
                                                __( 'Preview', 'wordpress-seo' )
×
857
                                        );
×
858
                                }
859
                        }
860
                        elseif ( $rec->post_status !== 'trash' ) {
×
861
                                $actions['view'] = sprintf(
×
862
                                        '<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
×
863
                                        esc_url( get_permalink( $rec->ID ) ),
×
864
                                        /* translators: Hidden accessibility text; %s: post title. */
865
                                        esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
×
866
                                        __( 'View', 'wordpress-seo' )
×
867
                                );
×
868
                        }
869
                }
870

871
                $return .= $this->row_actions( $actions );
×
872

873
                return $return;
×
874
        }
875

876
        /**
877
         * Parsing the column based on the $column_name.
878
         *
879
         * @param string  $column_name Column name.
880
         * @param WP_Post $rec         Post object.
881
         *
882
         * @return string
883
         */
884
        protected function parse_column( $column_name, $rec ) {
×
885

886
                static $date_format;
×
887

888
                if ( ! isset( $date_format ) ) {
×
889
                        $date_format = get_option( 'date_format' );
×
890
                }
891

892
                switch ( $column_name ) {
893
                        case 'col_page_title':
×
894
                                $column_value = $this->parse_page_title_column( $rec );
×
895
                                break;
×
896

897
                        case 'col_page_slug':
×
898
                                $permalink    = get_permalink( $rec->ID );
×
899
                                $display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink );
×
900
                                $column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) );
×
901
                                break;
×
902

903
                        case 'col_post_type':
×
904
                                $post_type    = get_post_type_object( $rec->post_type );
×
905
                                $column_value = $post_type->labels->singular_name;
×
906
                                break;
×
907

908
                        case 'col_post_status':
×
909
                                $post_status  = get_post_status_object( $rec->post_status );
×
910
                                $column_value = $post_status->label;
×
911
                                break;
×
912

913
                        case 'col_post_date':
×
914
                                $column_value = date_i18n( $date_format, strtotime( $rec->post_date ) );
×
915
                                break;
×
916

917
                        case 'col_row_action':
×
918
                                $column_value = sprintf(
×
919
                                        '<a href="#" role="button" class="wpseo-save" data-id="%1$s">%2$s</a> <span aria-hidden="true">|</span> <a href="#" role="button" class="wpseo-save-all">%3$s</a>',
×
920
                                        $rec->ID,
×
921
                                        esc_html__( 'Save', 'wordpress-seo' ),
×
922
                                        esc_html__( 'Save all', 'wordpress-seo' )
×
923
                                );
×
924
                                break;
×
925
                }
926

927
                if ( ! empty( $column_value ) ) {
×
928
                        return $column_value;
×
929
                }
930
        }
931

932
        /**
933
         * Parse the field where the existing meta-data value is displayed.
934
         *
935
         * @param int        $record_id  Record ID.
936
         * @param string     $attributes HTML attributes.
937
         * @param bool|array $values     Optional values data array.
938
         *
939
         * @return string
940
         */
941
        protected function parse_meta_data_field( $record_id, $attributes, $values = false ) {
×
942

943
                // Fill meta data if exists in $this->meta_data.
944
                $meta_data  = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : [];
×
945
                $meta_key   = WPSEO_Meta::$meta_prefix . $this->target_db_field;
×
946
                $meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : '';
×
947

948
                if ( ! empty( $values ) ) {
×
949
                        $meta_value = $values[ $meta_value ];
×
950
                }
951

952
                $id = "wpseo-existing-$this->target_db_field-$record_id";
×
953

954
                // $attributes correctly escaped, verified by Alexander. See WPSEO_Bulk_Description_List_Table::parse_page_specific_column.
955
                return sprintf( '<td %2$s id="%3$s">%1$s</td>', esc_html( $meta_value ), $attributes, esc_attr( $id ) );
×
956
        }
957

958
        /**
959
         * Method for setting the meta data, which belongs to the records that will be shown on the current page.
960
         *
961
         * This method will loop through the current items ($this->items) for getting the post_id. With this data
962
         * ($needed_ids) the method will query the meta-data table for getting the title.
963
         *
964
         * @return void
965
         */
966
        protected function get_meta_data() {
×
967

968
                $post_ids  = $this->get_post_ids();
×
969
                $meta_data = $this->get_meta_data_result( $post_ids );
×
970

971
                $this->parse_meta_data( $meta_data );
×
972

973
                // Little housekeeping.
974
                unset( $post_ids, $meta_data );
×
975
        }
976

977
        /**
978
         * Getting all post_ids from to $this->items.
979
         *
980
         * @return array
981
         */
982
        protected function get_post_ids() {
×
983
                $post_ids = [];
×
984
                foreach ( $this->items as $item ) {
×
985
                        $post_ids[] = $item->ID;
×
986
                }
987

988
                return $post_ids;
×
989
        }
990

991
        /**
992
         * Getting the meta_data from database.
993
         *
994
         * @param array $post_ids Post IDs for SQL IN part.
995
         *
996
         * @return mixed
997
         */
998
        protected function get_meta_data_result( array $post_ids ) {
×
999
                global $wpdb;
×
1000

1001
                $where = $wpdb->prepare(
×
1002
                        'post_id IN (' . implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) ) . ')',
×
1003
                        $post_ids
×
1004
                );
×
1005

1006
                $where .= $wpdb->prepare( ' AND meta_key = %s', WPSEO_Meta::$meta_prefix . $this->target_db_field );
×
1007

1008
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- They are prepared on the lines above.
1009
                return $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE {$where}" );
×
1010
        }
1011

1012
        /**
1013
         * Setting $this->meta_data.
1014
         *
1015
         * @param array $meta_data Meta data set.
1016
         *
1017
         * @return void
1018
         */
1019
        protected function parse_meta_data( $meta_data ) {
×
1020

1021
                foreach ( $meta_data as $row ) {
×
1022
                        $this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
×
1023
                }
1024
        }
1025

1026
        /**
1027
         * This method will merge general array with given parameter $columns.
1028
         *
1029
         * @param array $columns Optional columns set.
1030
         *
1031
         * @return array
1032
         */
1033
        protected function merge_columns( $columns = [] ) {
×
1034
                $columns = array_merge(
×
1035
                        [
×
1036
                                'col_page_title'  => __( 'WP Page Title', 'wordpress-seo' ),
×
1037
                                'col_post_type'   => __( 'Content Type', 'wordpress-seo' ),
×
1038
                                'col_post_status' => __( 'Post Status', 'wordpress-seo' ),
×
1039
                                'col_post_date'   => __( 'Publication date', 'wordpress-seo' ),
×
1040
                                'col_page_slug'   => __( 'Page URL/Slug', 'wordpress-seo' ),
×
1041
                        ],
×
1042
                        $columns
×
1043
                );
×
1044

1045
                $columns['col_row_action'] = __( 'Action', 'wordpress-seo' );
×
1046

1047
                return $columns;
×
1048
        }
1049
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc