PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 28.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v28.5
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / admin / class-bulk-editor-list-table.php

class-bulk-editor-list-table.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 28.5, at admin/class-bulk-editor-list-table.php

1,055 lines 29.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 * @deprecated 28.1
13 * @codeCoverageIgnore
14 */
15 class WPSEO_Bulk_List_Table extends WP_List_Table {
16
17 /**
18 * The nonce that was passed with the request.
19 *
20 * @var string
21 */
22 private $nonce;
23
24 /**
25 * Array of post types for which the current user has `edit_others_posts` capabilities.
26 *
27 * @var array
28 */
29 private $all_posts;
30
31 /**
32 * Array of post types for which the current user has `edit_posts` capabilities, but not `edit_others_posts`.
33 *
34 * @var array
35 */
36 private $own_posts;
37
38 /**
39 * Saves all the metadata into this array.
40 *
41 * @var array
42 */
43 protected $meta_data = [];
44
45 /**
46 * The current requested page_url.
47 *
48 * @var string
49 */
50 private $request_url = '';
51
52 /**
53 * The current page (depending on $_GET['paged']) if current tab is for current page_type, else it will be 1.
54 *
55 * @var int
56 */
57 private $current_page;
58
59 /**
60 * The current post filter, if is used (depending on $_GET['post_type_filter']).
61 *
62 * @var string
63 */
64 private $current_filter;
65
66 /**
67 * The current post status, if is used (depending on $_GET['post_status']).
68 *
69 * @var string
70 */
71 private $current_status;
72
73 /**
74 * The current sorting, if used (depending on $_GET['order'] and $_GET['orderby']).
75 *
76 * @var string
77 */
78 private $current_order;
79
80 /**
81 * The page_type for current class instance (for example: title / description).
82 *
83 * @var string
84 */
85 protected $page_type;
86
87 /**
88 * Based on the page_type ($this->page_type) there will be constructed an url part, for subpages and
89 * navigation.
90 *
91 * @var string
92 */
93 protected $page_url;
94
95 /**
96 * The settings which will be used in the __construct.
97 *
98 * @var array
99 */
100 protected $settings;
101
102 /**
103 * Holds the pagination config.
104 *
105 * @var array
106 */
107 protected $pagination = [];
108
109 /**
110 * Holds the sanitized data from the user input.
111 *
112 * @var array
113 */
114 protected $input_fields = [];
115
116 /**
117 * The field in the database where meta field is saved.
118 *
119 * Should be set in the child class.
120 *
121 * @var string
122 */
123 protected $target_db_field = '';
124
125 /**
126 * Class constructor.
127 *
128 * @param array $args The arguments.
129 */
130 public function __construct( $args = [] ) {
131 _deprecated_function( __METHOD__, 'Yoast SEO 28.1' );
132
133 parent::__construct( $this->settings );
134
135 $args = wp_parse_args(
136 $args,
137 [
138 'nonce' => '',
139 'input_fields' => [],
140 ],
141 );
142
143 $this->input_fields = $args['input_fields'];
144 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
145 $this->request_url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
146 }
147
148 $this->current_page = ( ! empty( $this->input_fields['paged'] ) ) ? $this->input_fields['paged'] : 1;
149 $this->current_filter = ( ! empty( $this->input_fields['post_type_filter'] ) ) ? $this->input_fields['post_type_filter'] : 1;
150 $this->current_status = ( ! empty( $this->input_fields['post_status'] ) ) ? $this->input_fields['post_status'] : 1;
151 $this->current_order = [
152 'order' => ( ! empty( $this->input_fields['order'] ) ) ? $this->input_fields['order'] : 'asc',
153 'orderby' => ( ! empty( $this->input_fields['orderby'] ) ) ? $this->input_fields['orderby'] : 'post_title',
154 ];
155
156 $this->nonce = $args['nonce'];
157 $this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}";
158
159 $this->populate_editable_post_types();
160 }
161
162 /**
163 * Prepares the data and renders the page.
164 *
165 * @return void
166 */
167 public function show_page() {
168 $this->prepare_page_navigation();
169 $this->prepare_items();
170
171 $this->views();
172 $this->display();
173 }
174
175 /**
176 * Used in the constructor to build a reference list of post types the current user can edit.
177 *
178 * @return void
179 */
180 protected function populate_editable_post_types() {
181 $post_types = get_post_types(
182 [
183 'public' => true,
184 'exclude_from_search' => false,
185 ],
186 'object',
187 );
188
189 $this->all_posts = [];
190 $this->own_posts = [];
191
192 if ( is_array( $post_types ) && $post_types !== [] ) {
193 foreach ( $post_types as $post_type ) {
194 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
195 continue;
196 }
197
198 if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
199 $this->all_posts[] = esc_sql( $post_type->name );
200 }
201 else {
202 $this->own_posts[] = esc_sql( $post_type->name );
203 }
204 }
205 }
206 }
207
208 /**
209 * Will show the navigation for the table like page navigation and page filter.
210 *
211 * @param string $which Table nav location (such as top).
212 *
213 * @return void
214 */
215 public function display_tablenav( $which ) {
216 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
217 $post_status = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : '';
218 $order_by = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';
219 $order = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : '';
220 $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
221 // phpcs:enable WordPress.Security.NonceVerification.Recommended;
222 ?>
223 <div class="tablenav <?php echo esc_attr( $which ); ?>">
224
225 <?php if ( $which === 'top' ) { ?>
226 <form id="posts-filter" action="" method="get">
227 <input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>" />
228 <input type="hidden" name="page" value="wpseo_tools" />
229 <input type="hidden" name="tool" value="bulk-editor" />
230 <input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>" />
231 <input type="hidden" name="orderby"
232 value="<?php echo esc_attr( $order_by ); ?>" />
233 <input type="hidden" name="order"
234 value="<?php echo esc_attr( $order ); ?>" />
235 <input type="hidden" name="post_type_filter"
236 value="<?php echo esc_attr( $post_type_filter ); ?>" />
237 <?php if ( ! empty( $post_status ) ) { ?>
238 <input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>" />
239 <?php } ?>
240 <?php } ?>
241
242 <?php
243 $this->extra_tablenav( $which );
244 $this->pagination( $which );
245 ?>
246
247 <br class="clear"/>
248 <?php if ( $which === 'top' ) { ?>
249 </form>
250 <?php } ?>
251 </div>
252
253 <?php
254 }
255
256 /**
257 * This function builds the base sql subquery used in this class.
258 *
259 * This function takes into account the post types in which the current user can
260 * edit all posts, and the ones the current user can only edit his/her own.
261 *
262 * @return string The subquery, which should always be used in $wpdb->prepare(),
263 * passing the current user_id in as the first parameter.
264 */
265 public function get_base_subquery() {
266 global $wpdb;
267
268 $all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'";
269 $own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'";
270
271 $post_author = esc_sql( (int) get_current_user_id() );
272
273 $subquery = "(
274 SELECT *
275 FROM {$wpdb->posts}
276 WHERE post_type IN ({$all_posts_string})
277 UNION ALL
278 SELECT *
279 FROM {$wpdb->posts}
280 WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author}
281 ) sub_base";
282
283 return $subquery;
284 }
285
286 /**
287 * Gets the views.
288 *
289 * @return array The views.
290 */
291 public function get_views() {
292 global $wpdb;
293
294 $status_links = [];
295
296 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
297 $subquery = $this->get_base_subquery();
298
299 $total_posts = $wpdb->get_var(
300 $wpdb->prepare(
301 "SELECT COUNT(ID) FROM {$subquery}
302 WHERE post_status IN ("
303 . implode( ', ', array_fill( 0, count( $states ), '%s' ) )
304 . ')',
305 $states,
306 ),
307 );
308
309 $post_status = isset( $_GET['post_status'] ) && is_string( $_GET['post_status'] ) ? sanitize_text_field( wp_unslash( $_GET['post_status'] ) ) : '';
310 $current_link_attributes = empty( $post_status ) ? ' class="current" aria-current="page"' : '';
311 $localized_text = sprintf(
312 /* translators: %s expands to the number of posts in localized format. */
313 _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ),
314 number_format_i18n( $total_posts ),
315 );
316
317 $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>';
318
319 $post_stati = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );
320 if ( is_array( $post_stati ) && $post_stati !== [] ) {
321 foreach ( $post_stati as $status ) {
322
323 $status_name = esc_sql( $status->name );
324
325 $total = (int) $wpdb->get_var(
326 $wpdb->prepare(
327 "
328 SELECT COUNT(ID) FROM {$subquery}
329 WHERE post_status = %s
330 ",
331 $status_name,
332 ),
333 );
334
335 if ( $total === 0 ) {
336 continue;
337 }
338
339 $current_link_attributes = '';
340 if ( $status_name === $post_status ) {
341 $current_link_attributes = ' class="current" aria-current="page"';
342 }
343
344 $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>';
345 }
346 }
347 unset( $post_stati, $status, $status_name, $total, $current_link_attributes );
348
349 $trashed_posts = $wpdb->get_var(
350 "SELECT COUNT(ID) FROM {$subquery}
351 WHERE post_status IN ('trash')
352 ",
353 );
354
355 $current_link_attributes = '';
356 if ( $post_status === 'trash' ) {
357 $current_link_attributes = 'class="current" aria-current="page"';
358 }
359
360 $localized_text = sprintf(
361 /* translators: %s expands to the number of trashed posts in localized format. */
362 _nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ),
363 number_format_i18n( $trashed_posts ),
364 );
365
366 $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>';
367
368 return $status_links;
369 }
370
371 /**
372 * Outputs extra table navigation.
373 *
374 * @param string $which Table nav location (such as top).
375 *
376 * @return void
377 */
378 public function extra_tablenav( $which ) {
379
380 if ( $which === 'top' ) {
381 $post_types = get_post_types(
382 [
383 'public' => true,
384 'exclude_from_search' => false,
385 ],
386 );
387
388 $instance_type = esc_attr( $this->page_type );
389
390 if ( is_array( $post_types ) && $post_types !== [] ) {
391 global $wpdb;
392
393 echo '<div class="alignleft actions">';
394
395 $post_types = esc_sql( $post_types );
396 $post_types = "'" . implode( "', '", $post_types ) . "'";
397
398 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
399 $states['trash'] = 'trash';
400
401 $subquery = $this->get_base_subquery();
402
403 $post_types = $wpdb->get_results(
404 $wpdb->prepare(
405 "SELECT DISTINCT post_type FROM {$subquery}
406 WHERE post_status IN ("
407 . implode( ', ', array_fill( 0, count( $states ), '%s' ) )
408 . ') ORDER BY post_type ASC',
409 $states,
410 ),
411 );
412
413 $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
414 $selected = ( ! empty( $post_type_filter ) ) ? $post_type_filter : '-1';
415
416 $options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>';
417
418 if ( is_array( $post_types ) && $post_types !== [] ) {
419 foreach ( $post_types as $post_type ) {
420 $obj = get_post_type_object( $post_type->post_type );
421 $options .= sprintf(
422 '<option value="%2$s" %3$s>%1$s</option>',
423 esc_html( $obj->labels->name ),
424 esc_attr( $post_type->post_type ),
425 selected( $selected, $post_type->post_type, false ),
426 );
427 }
428 }
429
430 printf(
431 '<label for="%1$s" class="screen-reader-text">%2$s</label>',
432 esc_attr( 'post-type-filter-' . $instance_type ),
433 /* translators: Hidden accessibility text. */
434 esc_html__( 'Filter by content type', 'wordpress-seo' ),
435 );
436 printf(
437 '<select name="post_type_filter" id="%2$s">%1$s</select>',
438 // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $options is properly escaped above.
439 $options,
440 esc_attr( 'post-type-filter-' . $instance_type ),
441 );
442
443 submit_button( esc_html__( 'Filter', 'wordpress-seo' ), 'button', false, false, [ 'id' => 'post-query-submit' ] );
444 echo '</div>';
445 }
446 }
447 }
448
449 /**
450 * Gets a list of sortable columns.
451 *
452 * The format is: 'internal-name' => array( 'orderby', bool ).
453 *
454 * @return array
455 */
456 public function get_sortable_columns() {
457 return [
458 'col_page_title' => [ 'post_title', true ],
459 'col_post_type' => [ 'post_type', false ],
460 'col_post_date' => [ 'post_date', false ],
461 ];
462 }
463
464 /**
465 * Sets the correct pagenumber and pageurl for the navigation.
466 *
467 * @return void
468 */
469 public function prepare_page_navigation() {
470
471 $request_url = $this->request_url . $this->page_url;
472
473 $current_page = $this->current_page;
474 $current_filter = $this->current_filter;
475 $current_status = $this->current_status;
476 $current_order = $this->current_order;
477
478 /*
479 * If current type doesn't compare with objects page_type, then we have to unset
480 * some vars in the requested url (which will be used for internal table urls).
481 */
482 if ( isset( $this->input_fields['type'] ) && $this->input_fields['type'] !== $this->page_type ) {
483 $request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below.
484 $request_url = remove_query_arg( 'post_type_filter', $request_url );
485 $request_url = remove_query_arg( 'post_status', $request_url );
486 $request_url = remove_query_arg( 'orderby', $request_url );
487 $request_url = remove_query_arg( 'order', $request_url );
488 $request_url = add_query_arg( 'pages', 1, $request_url );
489
490 $current_page = 1;
491 $current_filter = '-1';
492 $current_status = '';
493 $current_order = [
494 'orderby' => 'post_title',
495 'order' => 'asc',
496 ];
497 }
498
499 $_SERVER['REQUEST_URI'] = $request_url;
500
501 $_GET['paged'] = $current_page;
502 $_REQUEST['paged'] = $current_page;
503 $_REQUEST['post_type_filter'] = $current_filter;
504 $_GET['post_type_filter'] = $current_filter;
505 $_GET['post_status'] = $current_status;
506 $_GET['orderby'] = $current_order['orderby'];
507 $_GET['order'] = $current_order['order'];
508 }
509
510 /**
511 * Preparing the requested pagerows and setting the needed variables.
512 *
513 * @return void
514 */
515 public function prepare_items() {
516
517 $post_type_clause = $this->get_post_type_clause();
518 $all_states = $this->get_all_states();
519 $subquery = $this->get_base_subquery();
520
521 // Setting the column headers.
522 $this->set_column_headers();
523
524 // Count the total number of needed items and setting pagination given $total_items.
525 $total_items = $this->count_items( $subquery, $all_states, $post_type_clause );
526 $this->set_pagination( $total_items );
527
528 // Getting items given $query.
529 $query = $this->parse_item_query( $subquery, $all_states, $post_type_clause );
530 $this->get_items( $query );
531
532 // Get the metadata for the current items ($this->items).
533 $this->get_meta_data();
534 }
535
536 /**
537 * Getting the columns for first row.
538 *
539 * @return array
540 */
541 public function get_columns() {
542 return $this->merge_columns();
543 }
544
545 /**
546 * Setting the column headers.
547 *
548 * @return void
549 */
550 protected function set_column_headers() {
551 $columns = $this->get_columns();
552 $hidden = [];
553 $sortable = $this->get_sortable_columns();
554 $this->_column_headers = [ $columns, $hidden, $sortable ];
555 }
556
557 /**
558 * Counting total items.
559 *
560 * @param string $subquery SQL FROM part.
561 * @param string $all_states SQL IN part.
562 * @param string $post_type_clause SQL post type part.
563 *
564 * @return mixed
565 */
566 protected function count_items( $subquery, $all_states, $post_type_clause ) {
567 global $wpdb;
568
569 return (int) $wpdb->get_var(
570 "SELECT COUNT(ID) FROM {$subquery}
571 WHERE post_status IN ({$all_states})
572 {$post_type_clause}
573 ",
574 );
575 }
576
577 /**
578 * Getting the post_type_clause filter.
579 *
580 * @return string
581 */
582 protected function get_post_type_clause() {
583 // Filter Block.
584 $post_type_clause = '';
585 $post_type_filter = isset( $_GET['post_type_filter'] ) && is_string( $_GET['post_type_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type_filter'] ) ) : '';
586
587 if ( ! empty( $post_type_filter ) && get_post_type_object( $post_type_filter ) ) {
588 $post_types = esc_sql( $post_type_filter );
589 $post_type_clause = "AND post_type IN ('{$post_types}')";
590 }
591
592 return $post_type_clause;
593 }
594
595 /**
596 * Setting the pagination.
597 *
598 * Total items is the number of all visible items.
599 *
600 * @param int $total_items Total items counts.
601 *
602 * @return void
603 */
604 protected function set_pagination( $total_items ) {
605 // Calculate items per page.
606 $per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 );
607 $paged = isset( $_GET['paged'] ) && is_string( $_GET['paged'] ) ? esc_sql( sanitize_text_field( wp_unslash( $_GET['paged'] ) ) ) : '';
608
609 if ( empty( $paged ) || ! is_numeric( $paged ) ) {
610 $paged = 1;
611 }
612 else {
613 $paged = (int) $paged;
614 }
615
616 if ( $paged <= 0 ) {
617 $paged = 1;
618 }
619
620 $this->set_pagination_args(
621 [
622 'total_items' => $total_items,
623 'total_pages' => ceil( $total_items / $per_page ),
624 'per_page' => $per_page,
625 ],
626 );
627
628 $this->pagination = [
629 'per_page' => $per_page,
630 'offset' => ( ( $paged - 1 ) * $per_page ),
631 ];
632 }
633
634 /**
635 * Parse the query to get items from database.
636 *
637 * Based on given parameters there will be parse a query which will get all the pages/posts and other post_types
638 * from the database.
639 *
640 * @param string $subquery SQL FROM part.
641 * @param string $all_states SQL IN part.
642 * @param string $post_type_clause SQL post type part.
643 *
644 * @return string
645 */
646 protected function parse_item_query( $subquery, $all_states, $post_type_clause ) {
647 // Order By block.
648 $orderby = isset( $_GET['orderby'] ) && is_string( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';
649
650 $orderby = ! empty( $orderby ) ? esc_sql( $orderby ) : 'post_title';
651 $orderby = $this->sanitize_orderby( $orderby );
652
653 // Order clause.
654 $order = isset( $_GET['order'] ) && is_string( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : '';
655 $order = ! empty( $order ) ? esc_sql( strtoupper( $order ) ) : 'ASC';
656 $order = $this->sanitize_order( $order );
657
658 // Get all needed results.
659 $query = "
660 SELECT ID, post_title, post_type, post_status, post_modified, post_date
661 FROM {$subquery}
662 WHERE post_status IN ({$all_states}) $post_type_clause
663 ORDER BY {$orderby} {$order}
664 LIMIT %d,%d
665 ";
666
667 return $query;
668 }
669
670 /**
671 * Heavily restricts the possible columns by which a user can order the table
672 * in the bulk editor, thereby preventing a possible CSRF vulnerability.
673 *
674 * @param string $orderby The column by which we want to order.
675 *
676 * @return string
677 */
678 protected function sanitize_orderby( $orderby ) {
679 $valid_column_names = [
680 'post_title',
681 'post_type',
682 'post_date',
683 ];
684
685 if ( in_array( $orderby, $valid_column_names, true ) ) {
686 return $orderby;
687 }
688
689 return 'post_title';
690 }
691
692 /**
693 * Makes sure the order clause is always ASC or DESC for the bulk editor table,
694 * thereby preventing a possible CSRF vulnerability.
695 *
696 * @param string $order Whether we want to sort ascending or descending.
697 *
698 * @return string SQL order string (ASC, DESC).
699 */
700 protected function sanitize_order( $order ) {
701 if ( in_array( strtoupper( $order ), [ 'ASC', 'DESC' ], true ) ) {
702 return $order;
703 }
704
705 return 'ASC';
706 }
707
708 /**
709 * Getting all the items.
710 *
711 * @param string $query SQL query to use.
712 *
713 * @return void
714 */
715 protected function get_items( $query ) {
716 global $wpdb;
717
718 $this->items = $wpdb->get_results(
719 $wpdb->prepare(
720 $query,
721 $this->pagination['offset'],
722 $this->pagination['per_page'],
723 ),
724 );
725 }
726
727 /**
728 * Getting all the states.
729 *
730 * @return string
731 */
732 protected function get_all_states() {
733 global $wpdb;
734
735 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
736 $states['trash'] = 'trash';
737
738 if ( ! empty( $this->input_fields['post_status'] ) ) {
739 $requested_state = $this->input_fields['post_status'];
740 if ( in_array( $requested_state, $states, true ) ) {
741 $states = [ $requested_state ];
742 }
743
744 if ( $requested_state !== 'trash' ) {
745 unset( $states['trash'] );
746 }
747 }
748
749 return $wpdb->prepare(
750 implode( ', ', array_fill( 0, count( $states ), '%s' ) ),
751 $states,
752 );
753 }
754
755 /**
756 * Based on $this->items and the defined columns, the table rows will be displayed.
757 *
758 * @return void
759 */
760 public function display_rows() {
761
762 $records = $this->items;
763
764 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
765
766 if ( ( is_array( $records ) && $records !== [] ) && ( is_array( $columns ) && $columns !== [] ) ) {
767
768 foreach ( $records as $record ) {
769
770 echo '<tr id="', esc_attr( 'record_' . $record->ID ), '">';
771
772 foreach ( $columns as $column_name => $column_display_name ) {
773
774 $classes = '';
775 if ( $primary === $column_name ) {
776 $classes .= ' has-row-actions column-primary';
777 }
778
779 $attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name );
780
781 $column_value = $this->parse_column( $column_name, $record );
782
783 if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) {
784 $column_value = $this->parse_page_specific_column( $column_name, $record, $attributes );
785 }
786
787 if ( ! empty( $column_value ) ) {
788 printf( '<td %2$s>%1$s</td>', $column_value, $attributes );
789 }
790 }
791
792 echo '</tr>';
793 }
794 }
795 }
796
797 /**
798 * Getting the attributes for each table cell.
799 *
800 * @param string $column_name Column name string.
801 * @param array $hidden Set of hidden columns.
802 * @param string $classes Additional CSS classes.
803 * @param string $column_display_name Column display name string.
804 *
805 * @return string
806 */
807 protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) {
808
809 $attributes = '';
810 $class = [ $column_name, "column-$column_name$classes" ];
811
812 if ( in_array( $column_name, $hidden, true ) ) {
813 $class[] = 'hidden';
814 }
815
816 if ( ! empty( $class ) ) {
817 $attributes = 'class="' . esc_attr( implode( ' ', $class ) ) . '"';
818 }
819
820 $attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"';
821
822 return $attributes;
823 }
824
825 /**
826 * Parsing the title.
827 *
828 * @param WP_Post $rec Post object.
829 *
830 * @return string
831 */
832 protected function parse_page_title_column( $rec ) {
833
834 $title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title;
835
836 $return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) );
837
838 $post_type_object = get_post_type_object( $rec->post_type );
839 $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $rec->ID );
840
841 $actions = [];
842
843 if ( $can_edit_post && $rec->post_status !== 'trash' ) {
844 $actions['edit'] = sprintf(
845 '<a href="%s" aria-label="%s">%s</a>',
846 esc_url( get_edit_post_link( $rec->ID, true ) ),
847 /* translators: Hidden accessibility text; %s: post title. */
848 esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
849 __( 'Edit', 'wordpress-seo' ),
850 );
851 }
852
853 if ( $post_type_object->public ) {
854 if ( in_array( $rec->post_status, [ 'pending', 'draft', 'future' ], true ) ) {
855 if ( $can_edit_post ) {
856 $actions['view'] = sprintf(
857 '<a href="%s" aria-label="%s">%s</a>',
858 esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ),
859 /* translators: Hidden accessibility text; %s: post title. */
860 esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
861 __( 'Preview', 'wordpress-seo' ),
862 );
863 }
864 }
865 elseif ( $rec->post_status !== 'trash' ) {
866 $actions['view'] = sprintf(
867 '<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
868 esc_url( get_permalink( $rec->ID ) ),
869 /* translators: Hidden accessibility text; %s: post title. */
870 esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
871 __( 'View', 'wordpress-seo' ),
872 );
873 }
874 }
875
876 $return .= $this->row_actions( $actions );
877
878 return $return;
879 }
880
881 /**
882 * Parsing the column based on the $column_name.
883 *
884 * @param string $column_name Column name.
885 * @param WP_Post $rec Post object.
886 *
887 * @return string
888 */
889 protected function parse_column( $column_name, $rec ) {
890
891 static $date_format;
892
893 if ( ! isset( $date_format ) ) {
894 $date_format = get_option( 'date_format' );
895 }
896
897 switch ( $column_name ) {
898 case 'col_page_title':
899 $column_value = $this->parse_page_title_column( $rec );
900 break;
901
902 case 'col_page_slug':
903 $permalink = get_permalink( $rec->ID );
904 $display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink );
905 $column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) );
906 break;
907
908 case 'col_post_type':
909 $post_type = get_post_type_object( $rec->post_type );
910 $column_value = $post_type->labels->singular_name;
911 break;
912
913 case 'col_post_status':
914 $post_status = get_post_status_object( $rec->post_status );
915 $column_value = $post_status->label;
916 break;
917
918 case 'col_post_date':
919 $column_value = date_i18n( $date_format, strtotime( $rec->post_date ) );
920 break;
921
922 case 'col_row_action':
923 $column_value = sprintf(
924 '<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>',
925 $rec->ID,
926 esc_html__( 'Save', 'wordpress-seo' ),
927 esc_html__( 'Save all', 'wordpress-seo' ),
928 );
929 break;
930 }
931
932 if ( ! empty( $column_value ) ) {
933 return $column_value;
934 }
935 }
936
937 /**
938 * Parse the field where the existing meta-data value is displayed.
939 *
940 * @param int $record_id Record ID.
941 * @param string $attributes HTML attributes.
942 * @param bool|array $values Optional values data array.
943 *
944 * @return string
945 */
946 protected function parse_meta_data_field( $record_id, $attributes, $values = false ) {
947
948 // Fill meta data if exists in $this->meta_data.
949 $meta_data = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : [];
950 $meta_key = WPSEO_Meta::$meta_prefix . $this->target_db_field;
951 $meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : '';
952
953 if ( ! empty( $values ) ) {
954 $meta_value = $values[ $meta_value ];
955 }
956
957 $id = "wpseo-existing-$this->target_db_field-$record_id";
958
959 // $attributes correctly escaped, verified by Alexander. See WPSEO_Bulk_Description_List_Table::parse_page_specific_column.
960 return sprintf( '<td %2$s id="%3$s">%1$s</td>', esc_html( $meta_value ), $attributes, esc_attr( $id ) );
961 }
962
963 /**
964 * Method for setting the meta data, which belongs to the records that will be shown on the current page.
965 *
966 * This method will loop through the current items ($this->items) for getting the post_id. With this data
967 * ($needed_ids) the method will query the meta-data table for getting the title.
968 *
969 * @return void
970 */
971 protected function get_meta_data() {
972
973 $post_ids = $this->get_post_ids();
974 $meta_data = $this->get_meta_data_result( $post_ids );
975
976 $this->parse_meta_data( $meta_data );
977
978 // Little housekeeping.
979 unset( $post_ids, $meta_data );
980 }
981
982 /**
983 * Getting all post_ids from to $this->items.
984 *
985 * @return array
986 */
987 protected function get_post_ids() {
988 $post_ids = [];
989 foreach ( $this->items as $item ) {
990 $post_ids[] = $item->ID;
991 }
992
993 return $post_ids;
994 }
995
996 /**
997 * Getting the meta_data from database.
998 *
999 * @param array $post_ids Post IDs for SQL IN part.
1000 *
1001 * @return mixed
1002 */
1003 protected function get_meta_data_result( array $post_ids ) {
1004 global $wpdb;
1005
1006 $where = $wpdb->prepare(
1007 'post_id IN (' . implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) ) . ')',
1008 $post_ids,
1009 );
1010
1011 $where .= $wpdb->prepare( ' AND meta_key = %s', WPSEO_Meta::$meta_prefix . $this->target_db_field );
1012
1013 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- They are prepared on the lines above.
1014 return $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE {$where}" );
1015 }
1016
1017 /**
1018 * Setting $this->meta_data.
1019 *
1020 * @param array $meta_data Meta data set.
1021 *
1022 * @return void
1023 */
1024 protected function parse_meta_data( $meta_data ) {
1025
1026 foreach ( $meta_data as $row ) {
1027 $this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
1028 }
1029 }
1030
1031 /**
1032 * This method will merge general array with given parameter $columns.
1033 *
1034 * @param array $columns Optional columns set.
1035 *
1036 * @return array
1037 */
1038 protected function merge_columns( $columns = [] ) {
1039 $columns = array_merge(
1040 [
1041 'col_page_title' => __( 'WP Page Title', 'wordpress-seo' ),
1042 'col_post_type' => __( 'Content Type', 'wordpress-seo' ),
1043 'col_post_status' => __( 'Post Status', 'wordpress-seo' ),
1044 'col_post_date' => __( 'Publication date', 'wordpress-seo' ),
1045 'col_page_slug' => __( 'Page URL/Slug', 'wordpress-seo' ),
1046 ],
1047 $columns,
1048 );
1049
1050 $columns['col_row_action'] = __( 'Action', 'wordpress-seo' );
1051
1052 return $columns;
1053 }
1054 }
1055