PluginProbe ʕ •ᴥ•ʔ
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 18.0
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v18.0
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 19.1 19.10 19.11 19.12 19.13 19.14 19.2 19.3 19.4 19.5 19.5.1 19.6 19.6.1 19.7 19.7.1 19.7.2 19.8 19.9 20.0 20.1 20.10 20.11 20.12 20.13 20.2 20.2.1 20.3 20.4 20.5 20.6 20.7 20.8 20.9 21.0 21.1 21.2 21.3 21.4 21.5 21.6 21.7 21.8 21.8.1 21.9 21.9.1 22.0 22.1 22.2 22.3 22.4 22.5 22.6 22.7 22.8 22.9 23.0 23.1 23.2 23.3 23.4 23.5 23.6 23.7 23.8 23.9 24.0 24.1 24.2 24.3 24.4 24.5 24.6 24.7 24.8 24.8.1 24.9 25.0 25.1 25.2 25.3 25.3.1 25.4 25.5 25.6 25.7 25.8 25.9 26.0 26.1 26.1.1 26.2 26.3 26.4 26.5 26.6 26.7 26.8 26.9 27.0 27.1 27.1.1 27.2 27.3 27.4
wordpress-seo / admin / class-bulk-editor-list-table.php
wordpress-seo / admin Last commit date
ajax 5 years ago capabilities 4 years ago endpoints 5 years ago exceptions 7 years ago filters 4 years ago formatter 4 years ago google_search_console 5 years ago import 4 years ago listeners 8 years ago menu 4 years ago metabox 4 years ago notifiers 4 years ago pages 4 years ago roles 5 years ago ryte 5 years ago services 5 years ago statistics 5 years ago taxonomy 4 years ago tracking 4 years ago views 4 years ago watchers 5 years ago admin-settings-changed-listener.php 5 years ago ajax.php 4 years ago class-admin-asset-analysis-worker-location.php 5 years ago class-admin-asset-dev-server-location.php 5 years ago class-admin-asset-location.php 8 years ago class-admin-asset-manager.php 4 years ago class-admin-asset-seo-location.php 4 years ago class-admin-asset-yoast-components-l10n.php 5 years ago class-admin-editor-specific-replace-vars.php 5 years ago class-admin-gutenberg-compatibility-notification.php 5 years ago class-admin-help-panel.php 5 years ago class-admin-init.php 4 years ago class-admin-recommended-replace-vars.php 6 years ago class-admin-user-profile.php 6 years ago class-admin-utils.php 5 years ago class-admin.php 4 years ago class-asset.php 5 years ago class-bulk-description-editor-list-table.php 5 years ago class-bulk-editor-list-table.php 4 years ago class-bulk-title-editor-list-table.php 6 years ago class-collector.php 6 years ago class-config.php 4 years ago class-customizer.php 5 years ago class-database-proxy.php 5 years ago class-export.php 5 years ago class-expose-shortlinks.php 4 years ago class-gutenberg-compatibility.php 4 years ago class-helpscout.php 5 years ago class-meta-columns.php 5 years ago class-my-yoast-proxy.php 5 years ago class-option-tab.php 4 years ago class-option-tabs-formatter.php 5 years ago class-option-tabs.php 5 years ago class-paper-presenter.php 5 years ago class-plugin-availability.php 5 years ago class-plugin-conflict.php 4 years ago class-premium-popup.php 5 years ago class-premium-upsell-admin-block.php 4 years ago class-primary-term-admin.php 5 years ago class-product-upsell-notice.php 5 years ago class-remote-request.php 5 years ago class-schema-person-upgrade-notification.php 4 years ago class-suggested-plugins.php 4 years ago class-yoast-columns.php 5 years ago class-yoast-dashboard-widget.php 4 years ago class-yoast-form.php 4 years ago class-yoast-input-validation.php 5 years ago class-yoast-network-admin.php 5 years ago class-yoast-network-settings-api.php 4 years ago class-yoast-notification-center.php 4 years ago class-yoast-notification.php 5 years ago class-yoast-notifications.php 5 years ago class-yoast-plugin-conflict.php 4 years ago index.php 10 years ago interface-collection.php 7 years ago interface-installable.php 8 years ago
class-bulk-editor-list-table.php
1006 lines
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 * Class constructor.
115 *
116 * @param array $args The arguments.
117 */
118 public function __construct( $args = [] ) {
119 parent::__construct( $this->settings );
120
121 $args = wp_parse_args(
122 $args,
123 [
124 'nonce' => '',
125 'input_fields' => [],
126 ]
127 );
128
129 $this->input_fields = $args['input_fields'];
130 if ( isset( $_SERVER['REQUEST_URI'] ) ) {
131 $this->request_url = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
132 }
133
134 $this->current_page = ( ! empty( $this->input_fields['paged'] ) ) ? $this->input_fields['paged'] : 1;
135 $this->current_filter = ( ! empty( $this->input_fields['post_type_filter'] ) ) ? $this->input_fields['post_type_filter'] : 1;
136 $this->current_status = ( ! empty( $this->input_fields['post_status'] ) ) ? $this->input_fields['post_status'] : 1;
137 $this->current_order = [
138 'order' => ( ! empty( $this->input_fields['order'] ) ) ? $this->input_fields['order'] : 'asc',
139 'orderby' => ( ! empty( $this->input_fields['orderby'] ) ) ? $this->input_fields['orderby'] : 'post_title',
140 ];
141
142 $this->nonce = $args['nonce'];
143 $this->page_url = "&nonce={$this->nonce}&type={$this->page_type}#top#{$this->page_type}";
144
145 $this->populate_editable_post_types();
146 }
147
148 /**
149 * Prepares the data and renders the page.
150 */
151 public function show_page() {
152 $this->prepare_page_navigation();
153 $this->prepare_items();
154
155 $this->views();
156 $this->display();
157 }
158
159 /**
160 * Used in the constructor to build a reference list of post types the current user can edit.
161 */
162 protected function populate_editable_post_types() {
163 $post_types = get_post_types(
164 [
165 'public' => true,
166 'exclude_from_search' => false,
167 ],
168 'object'
169 );
170
171 $this->all_posts = [];
172 $this->own_posts = [];
173
174 if ( is_array( $post_types ) && $post_types !== [] ) {
175 foreach ( $post_types as $post_type ) {
176 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
177 continue;
178 }
179
180 if ( current_user_can( $post_type->cap->edit_others_posts ) ) {
181 $this->all_posts[] = esc_sql( $post_type->name );
182 }
183 else {
184 $this->own_posts[] = esc_sql( $post_type->name );
185 }
186 }
187 }
188 }
189
190 /**
191 * Will show the navigation for the table like pagenavigation and pagefilter.
192 *
193 * @param string $which Table nav location (such as top).
194 */
195 public function display_tablenav( $which ) {
196 $post_status = sanitize_text_field( filter_input( INPUT_GET, 'post_status' ) );
197 ?>
198 <div class="tablenav <?php echo esc_attr( $which ); ?>">
199
200 <?php if ( $which === 'top' ) { ?>
201 <form id="posts-filter" action="" method="get">
202 <input type="hidden" name="nonce" value="<?php echo esc_attr( $this->nonce ); ?>"/>
203 <input type="hidden" name="page" value="wpseo_tools"/>
204 <input type="hidden" name="tool" value="bulk-editor"/>
205 <input type="hidden" name="type" value="<?php echo esc_attr( $this->page_type ); ?>"/>
206 <input type="hidden" name="orderby"
207 value="<?php echo esc_attr( filter_input( INPUT_GET, 'orderby' ) ); ?>"/>
208 <input type="hidden" name="order"
209 value="<?php echo esc_attr( filter_input( INPUT_GET, 'order' ) ); ?>"/>
210 <input type="hidden" name="post_type_filter"
211 value="<?php echo esc_attr( filter_input( INPUT_GET, 'post_type_filter' ) ); ?>"/>
212 <?php if ( ! empty( $post_status ) ) { ?>
213 <input type="hidden" name="post_status" value="<?php echo esc_attr( $post_status ); ?>"/>
214 <?php } ?>
215 <?php } ?>
216
217 <?php
218 $this->extra_tablenav( $which );
219 $this->pagination( $which );
220 ?>
221
222 <br class="clear"/>
223 <?php if ( $which === 'top' ) { ?>
224 </form>
225 <?php } ?>
226 </div>
227
228 <?php
229 }
230
231 /**
232 * This function builds the base sql subquery used in this class.
233 *
234 * This function takes into account the post types in which the current user can
235 * edit all posts, and the ones the current user can only edit his/her own.
236 *
237 * @return string The subquery, which should always be used in $wpdb->prepare(),
238 * passing the current user_id in as the first parameter.
239 */
240 public function get_base_subquery() {
241 global $wpdb;
242
243 $all_posts_string = "'" . implode( "', '", $this->all_posts ) . "'";
244 $own_posts_string = "'" . implode( "', '", $this->own_posts ) . "'";
245
246 $post_author = esc_sql( (int) get_current_user_id() );
247
248 $subquery = "(
249 SELECT *
250 FROM {$wpdb->posts}
251 WHERE post_type IN ({$all_posts_string})
252 UNION ALL
253 SELECT *
254 FROM {$wpdb->posts}
255 WHERE post_type IN ({$own_posts_string}) AND post_author = {$post_author}
256 ) sub_base";
257
258 return $subquery;
259 }
260
261 /**
262 * Gets the views.
263 *
264 * @return array The views.
265 */
266 public function get_views() {
267 global $wpdb;
268
269 $status_links = [];
270
271 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
272 $subquery = $this->get_base_subquery();
273
274 $total_posts = $wpdb->get_var(
275 $wpdb->prepare(
276 "SELECT COUNT(ID) FROM {$subquery}
277 WHERE post_status IN (" .
278 implode( ', ', array_fill( 0, count( $states ), '%s' ) ) .
279 ')',
280 $states
281 )
282 );
283
284 $post_status = filter_input( INPUT_GET, 'post_status' );
285 $current_link_attributes = empty( $post_status ) ? ' class="current" aria-current="page"' : '';
286 $localized_text = sprintf(
287 /* translators: %s expands to the number of posts in localized format. */
288 _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $total_posts, 'posts', 'wordpress-seo' ),
289 number_format_i18n( $total_posts )
290 );
291
292 $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>';
293
294 $post_stati = get_post_stati( [ 'show_in_admin_all_list' => true ], 'objects' );
295 if ( is_array( $post_stati ) && $post_stati !== [] ) {
296 foreach ( $post_stati as $status ) {
297
298 $status_name = esc_sql( $status->name );
299
300 $total = (int) $wpdb->get_var(
301 $wpdb->prepare(
302 "
303 SELECT COUNT(ID) FROM {$subquery}
304 WHERE post_status = %s
305 ",
306 $status_name
307 )
308 );
309
310 if ( $total === 0 ) {
311 continue;
312 }
313
314 $current_link_attributes = '';
315 if ( $status_name === $post_status ) {
316 $current_link_attributes = ' class="current" aria-current="page"';
317 }
318
319 $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>';
320 }
321 }
322 unset( $post_stati, $status, $status_name, $total, $current_link_attributes );
323
324 $trashed_posts = $wpdb->get_var(
325 "SELECT COUNT(ID) FROM {$subquery}
326 WHERE post_status IN ('trash')
327 "
328 );
329
330 $current_link_attributes = '';
331 if ( $post_status === 'trash' ) {
332 $current_link_attributes = 'class="current" aria-current="page"';
333 }
334
335 $localized_text = sprintf(
336 /* translators: %s expands to the number of trashed posts in localized format. */
337 _nx( 'Trash <span class="count">(%s)</span>', 'Trash <span class="count">(%s)</span>', $trashed_posts, 'posts', 'wordpress-seo' ),
338 number_format_i18n( $trashed_posts )
339 );
340
341 $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>';
342
343 return $status_links;
344 }
345
346 /**
347 * Outputs extra table navigation.
348 *
349 * @param string $which Table nav location (such as top).
350 */
351 public function extra_tablenav( $which ) {
352
353 if ( $which === 'top' ) {
354 $post_types = get_post_types(
355 [
356 'public' => true,
357 'exclude_from_search' => false,
358 ]
359 );
360
361 $instance_type = esc_attr( $this->page_type );
362
363 if ( is_array( $post_types ) && $post_types !== [] ) {
364 global $wpdb;
365
366 echo '<div class="alignleft actions">';
367
368 $post_types = esc_sql( $post_types );
369 $post_types = "'" . implode( "', '", $post_types ) . "'";
370
371 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
372 $states['trash'] = 'trash';
373
374 $subquery = $this->get_base_subquery();
375
376 $post_types = $wpdb->get_results(
377 $wpdb->prepare(
378 "SELECT DISTINCT post_type FROM {$subquery}
379 WHERE post_status IN (" .
380 implode( ', ', array_fill( 0, count( $states ), '%s' ) ) .
381 ') ORDER BY post_type ASC',
382 $states
383 )
384 );
385
386 $post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
387 $selected = ( ! empty( $post_type_filter ) ) ? sanitize_text_field( $post_type_filter ) : '-1';
388
389 $options = '<option value="-1">' . esc_html__( 'Show All Content Types', 'wordpress-seo' ) . '</option>';
390
391 if ( is_array( $post_types ) && $post_types !== [] ) {
392 foreach ( $post_types as $post_type ) {
393 $obj = get_post_type_object( $post_type->post_type );
394 $options .= sprintf(
395 '<option value="%2$s" %3$s>%1$s</option>',
396 esc_html( $obj->labels->name ),
397 esc_attr( $post_type->post_type ),
398 selected( $selected, $post_type->post_type, false )
399 );
400 }
401 }
402
403 printf(
404 '<label for="%1$s" class="screen-reader-text">%2$s</label>',
405 esc_attr( 'post-type-filter-' . $instance_type ),
406 esc_html__( 'Filter by content type', 'wordpress-seo' )
407 );
408 printf(
409 '<select name="post_type_filter" id="%2$s">%1$s</select>',
410 // phpcs:ignore WordPress.Security.EscapeOutput -- Reason: $options is properly escaped above.
411 $options,
412 esc_attr( 'post-type-filter-' . $instance_type )
413 );
414
415 submit_button( esc_html__( 'Filter', 'wordpress-seo' ), 'button', false, false, [ 'id' => 'post-query-submit' ] );
416 echo '</div>';
417 }
418 }
419 }
420
421 /**
422 * Gets a list of sortable columns.
423 *
424 * The format is: 'internal-name' => array( 'orderby', bool ).
425 *
426 * @return array
427 */
428 public function get_sortable_columns() {
429 return [
430 'col_page_title' => [ 'post_title', true ],
431 'col_post_type' => [ 'post_type', false ],
432 'col_post_date' => [ 'post_date', false ],
433 ];
434 }
435
436 /**
437 * Sets the correct pagenumber and pageurl for the navigation.
438 */
439 public function prepare_page_navigation() {
440
441 $request_url = $this->request_url . $this->page_url;
442
443 $current_page = $this->current_page;
444 $current_filter = $this->current_filter;
445 $current_status = $this->current_status;
446 $current_order = $this->current_order;
447
448 /*
449 * If current type doesn't compare with objects page_type, then we have to unset
450 * some vars in the requested url (which will be used for internal table urls).
451 */
452 if ( isset( $this->input_fields['type'] ) && $this->input_fields['type'] !== $this->page_type ) {
453 $request_url = remove_query_arg( 'paged', $request_url ); // Page will be set with value 1 below.
454 $request_url = remove_query_arg( 'post_type_filter', $request_url );
455 $request_url = remove_query_arg( 'post_status', $request_url );
456 $request_url = remove_query_arg( 'orderby', $request_url );
457 $request_url = remove_query_arg( 'order', $request_url );
458 $request_url = add_query_arg( 'pages', 1, $request_url );
459
460 $current_page = 1;
461 $current_filter = '-1';
462 $current_status = '';
463 $current_order = [
464 'orderby' => 'post_title',
465 'order' => 'asc',
466 ];
467 }
468
469 $_SERVER['REQUEST_URI'] = $request_url;
470
471 $_GET['paged'] = $current_page;
472 $_REQUEST['paged'] = $current_page;
473 $_REQUEST['post_type_filter'] = $current_filter;
474 $_GET['post_type_filter'] = $current_filter;
475 $_GET['post_status'] = $current_status;
476 $_GET['orderby'] = $current_order['orderby'];
477 $_GET['order'] = $current_order['order'];
478 }
479
480 /**
481 * Preparing the requested pagerows and setting the needed variables.
482 */
483 public function prepare_items() {
484
485 $post_type_clause = $this->get_post_type_clause();
486 $all_states = $this->get_all_states();
487 $subquery = $this->get_base_subquery();
488
489 // Setting the column headers.
490 $this->set_column_headers();
491
492 // Count the total number of needed items and setting pagination given $total_items.
493 $total_items = $this->count_items( $subquery, $all_states, $post_type_clause );
494 $this->set_pagination( $total_items );
495
496 // Getting items given $query.
497 $query = $this->parse_item_query( $subquery, $all_states, $post_type_clause );
498 $this->get_items( $query );
499
500 // Get the metadata for the current items ($this->items).
501 $this->get_meta_data();
502 }
503
504 /**
505 * Getting the columns for first row.
506 *
507 * @return array
508 */
509 public function get_columns() {
510 return $this->merge_columns();
511 }
512
513 /**
514 * Setting the column headers.
515 */
516 protected function set_column_headers() {
517 $columns = $this->get_columns();
518 $hidden = [];
519 $sortable = $this->get_sortable_columns();
520 $this->_column_headers = [ $columns, $hidden, $sortable ];
521 }
522
523 /**
524 * Counting total items.
525 *
526 * @param string $subquery SQL FROM part.
527 * @param string $all_states SQL IN part.
528 * @param string $post_type_clause SQL post type part.
529 *
530 * @return mixed
531 */
532 protected function count_items( $subquery, $all_states, $post_type_clause ) {
533 global $wpdb;
534
535 return (int) $wpdb->get_var(
536 "SELECT COUNT(ID) FROM {$subquery}
537 WHERE post_status IN ({$all_states})
538 {$post_type_clause}
539 "
540 );
541 }
542
543 /**
544 * Getting the post_type_clause filter.
545 *
546 * @return string
547 */
548 protected function get_post_type_clause() {
549 // Filter Block.
550 $post_types = null;
551 $post_type_clause = '';
552 $post_type_filter = filter_input( INPUT_GET, 'post_type_filter' );
553
554 if ( ! empty( $post_type_filter ) && get_post_type_object( sanitize_text_field( $post_type_filter ) ) ) {
555 $post_types = esc_sql( sanitize_text_field( $post_type_filter ) );
556 $post_type_clause = "AND post_type IN ('{$post_types}')";
557 }
558
559 return $post_type_clause;
560 }
561
562 /**
563 * Setting the pagination.
564 *
565 * Total items is the number of all visible items.
566 *
567 * @param int $total_items Total items counts.
568 */
569 protected function set_pagination( $total_items ) {
570
571 // Calculate items per page.
572 $per_page = $this->get_items_per_page( 'wpseo_posts_per_page', 10 );
573 $paged = esc_sql( sanitize_text_field( filter_input( INPUT_GET, 'paged' ) ) );
574
575 if ( empty( $paged ) || ! is_numeric( $paged ) || $paged <= 0 ) {
576 $paged = 1;
577 }
578
579 $this->set_pagination_args(
580 [
581 'total_items' => $total_items,
582 'total_pages' => ceil( $total_items / $per_page ),
583 'per_page' => $per_page,
584 ]
585 );
586
587 $this->pagination = [
588 'per_page' => $per_page,
589 'offset' => ( ( $paged - 1 ) * $per_page ),
590 ];
591 }
592
593 /**
594 * Parse the query to get items from database.
595 *
596 * Based on given parameters there will be parse a query which will get all the pages/posts and other post_types
597 * from the database.
598 *
599 * @param string $subquery SQL FROM part.
600 * @param string $all_states SQL IN part.
601 * @param string $post_type_clause SQL post type part.
602 *
603 * @return string
604 */
605 protected function parse_item_query( $subquery, $all_states, $post_type_clause ) {
606 // Order By block.
607 $orderby = filter_input( INPUT_GET, 'orderby' );
608
609 $orderby = ! empty( $orderby ) ? esc_sql( sanitize_text_field( $orderby ) ) : 'post_title';
610 $orderby = $this->sanitize_orderby( $orderby );
611
612 // Order clause.
613 $order = filter_input( INPUT_GET, 'order' );
614 $order = ! empty( $order ) ? esc_sql( strtoupper( sanitize_text_field( $order ) ) ) : 'ASC';
615 $order = $this->sanitize_order( $order );
616
617 // Get all needed results.
618 $query = "
619 SELECT ID, post_title, post_type, post_status, post_modified, post_date
620 FROM {$subquery}
621 WHERE post_status IN ({$all_states}) $post_type_clause
622 ORDER BY {$orderby} {$order}
623 LIMIT %d,%d
624 ";
625
626 return $query;
627 }
628
629 /**
630 * Heavily restricts the possible columns by which a user can order the table
631 * in the bulk editor, thereby preventing a possible CSRF vulnerability.
632 *
633 * @param string $orderby The column by which we want to order.
634 *
635 * @return string
636 */
637 protected function sanitize_orderby( $orderby ) {
638 $valid_column_names = [
639 'post_title',
640 'post_type',
641 'post_date',
642 ];
643
644 if ( in_array( $orderby, $valid_column_names, true ) ) {
645 return $orderby;
646 }
647
648 return 'post_title';
649 }
650
651 /**
652 * Makes sure the order clause is always ASC or DESC for the bulk editor table,
653 * thereby preventing a possible CSRF vulnerability.
654 *
655 * @param string $order Whether we want to sort ascending or descending.
656 *
657 * @return string SQL order string (ASC, DESC).
658 */
659 protected function sanitize_order( $order ) {
660 if ( in_array( strtoupper( $order ), [ 'ASC', 'DESC' ], true ) ) {
661 return $order;
662 }
663
664 return 'ASC';
665 }
666
667 /**
668 * Getting all the items.
669 *
670 * @param string $query SQL query to use.
671 */
672 protected function get_items( $query ) {
673 global $wpdb;
674
675 $this->items = $wpdb->get_results(
676 $wpdb->prepare(
677 $query,
678 $this->pagination['offset'],
679 $this->pagination['per_page']
680 )
681 );
682 }
683
684 /**
685 * Getting all the states.
686 *
687 * @return string
688 */
689 protected function get_all_states() {
690 global $wpdb;
691
692 $states = get_post_stati( [ 'show_in_admin_all_list' => true ] );
693 $states['trash'] = 'trash';
694
695 if ( ! empty( $this->input_fields['post_status'] ) ) {
696 $requested_state = $this->input_fields['post_status'];
697 if ( in_array( $requested_state, $states, true ) ) {
698 $states = [ $requested_state ];
699 }
700
701 if ( $requested_state !== 'trash' ) {
702 unset( $states['trash'] );
703 }
704 }
705
706 return $wpdb->prepare(
707 implode( ', ', array_fill( 0, count( $states ), '%s' ) ),
708 $states
709 );
710 }
711
712 /**
713 * Based on $this->items and the defined columns, the table rows will be displayed.
714 */
715 public function display_rows() {
716
717 $records = $this->items;
718
719 list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info();
720
721 if ( ( is_array( $records ) && $records !== [] ) && ( is_array( $columns ) && $columns !== [] ) ) {
722
723 foreach ( $records as $record ) {
724
725 echo '<tr id="', esc_attr( 'record_' . $record->ID ), '">';
726
727 foreach ( $columns as $column_name => $column_display_name ) {
728
729 $classes = '';
730 if ( $primary === $column_name ) {
731 $classes .= ' has-row-actions column-primary';
732 }
733
734 $attributes = $this->column_attributes( $column_name, $hidden, $classes, $column_display_name );
735
736 $column_value = $this->parse_column( $column_name, $record );
737
738 if ( method_exists( $this, 'parse_page_specific_column' ) && empty( $column_value ) ) {
739 $column_value = $this->parse_page_specific_column( $column_name, $record, $attributes );
740 }
741
742 if ( ! empty( $column_value ) ) {
743 printf( '<td %2$s>%1$s</td>', $column_value, $attributes );
744 }
745 }
746
747 echo '</tr>';
748 }
749 }
750 }
751
752 /**
753 * Getting the attributes for each table cell.
754 *
755 * @param string $column_name Column name string.
756 * @param array $hidden Set of hidden columns.
757 * @param string $classes Additional CSS classes.
758 * @param string $column_display_name Column display name string.
759 *
760 * @return string
761 */
762 protected function column_attributes( $column_name, $hidden, $classes, $column_display_name ) {
763
764 $attributes = '';
765 $class = [ $column_name, "column-$column_name$classes" ];
766
767 if ( in_array( $column_name, $hidden, true ) ) {
768 $class[] = 'hidden';
769 }
770
771 if ( ! empty( $class ) ) {
772 $attributes = 'class="' . esc_attr( implode( ' ', $class ) ) . '"';
773 }
774
775 $attributes .= ' data-colname="' . esc_attr( $column_display_name ) . '"';
776
777 return $attributes;
778 }
779
780 /**
781 * Parsing the title.
782 *
783 * @param WP_Post $rec Post object.
784 *
785 * @return string
786 */
787 protected function parse_page_title_column( $rec ) {
788
789 $title = empty( $rec->post_title ) ? __( '(no title)', 'wordpress-seo' ) : $rec->post_title;
790
791 $return = sprintf( '<strong>%1$s</strong>', stripslashes( wp_strip_all_tags( $title ) ) );
792
793 $post_type_object = get_post_type_object( $rec->post_type );
794 $can_edit_post = current_user_can( $post_type_object->cap->edit_post, $rec->ID );
795
796 $actions = [];
797
798 if ( $can_edit_post && $rec->post_status !== 'trash' ) {
799 $actions['edit'] = sprintf(
800 '<a href="%s" aria-label="%s">%s</a>',
801 esc_url( get_edit_post_link( $rec->ID, true ) ),
802 /* translators: %s: post title */
803 esc_attr( sprintf( __( 'Edit &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
804 __( 'Edit', 'wordpress-seo' )
805 );
806 }
807
808 if ( $post_type_object->public ) {
809 if ( in_array( $rec->post_status, [ 'pending', 'draft', 'future' ], true ) ) {
810 if ( $can_edit_post ) {
811 $actions['view'] = sprintf(
812 '<a href="%s" aria-label="%s">%s</a>',
813 esc_url( add_query_arg( 'preview', 'true', get_permalink( $rec->ID ) ) ),
814 /* translators: %s: post title */
815 esc_attr( sprintf( __( 'Preview &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
816 __( 'Preview', 'wordpress-seo' )
817 );
818 }
819 }
820 elseif ( $rec->post_status !== 'trash' ) {
821 $actions['view'] = sprintf(
822 '<a href="%s" aria-label="%s" rel="bookmark">%s</a>',
823 esc_url( get_permalink( $rec->ID ) ),
824 /* translators: %s: post title */
825 esc_attr( sprintf( __( 'View &#8220;%s&#8221;', 'wordpress-seo' ), $title ) ),
826 __( 'View', 'wordpress-seo' )
827 );
828 }
829 }
830
831 $return .= $this->row_actions( $actions );
832
833 return $return;
834 }
835
836 /**
837 * Parsing the column based on the $column_name.
838 *
839 * @param string $column_name Column name.
840 * @param WP_Post $rec Post object.
841 *
842 * @return string
843 */
844 protected function parse_column( $column_name, $rec ) {
845
846 static $date_format;
847
848 if ( ! isset( $date_format ) ) {
849 $date_format = get_option( 'date_format' );
850 }
851
852 switch ( $column_name ) {
853 case 'col_page_title':
854 $column_value = $this->parse_page_title_column( $rec );
855 break;
856
857 case 'col_page_slug':
858 $permalink = get_permalink( $rec->ID );
859 $display_slug = str_replace( get_bloginfo( 'url' ), '', $permalink );
860 $column_value = sprintf( '<a href="%2$s" target="_blank">%1$s</a>', stripslashes( rawurldecode( $display_slug ) ), esc_url( $permalink ) );
861 break;
862
863 case 'col_post_type':
864 $post_type = get_post_type_object( $rec->post_type );
865 $column_value = $post_type->labels->singular_name;
866 break;
867
868 case 'col_post_status':
869 $post_status = get_post_status_object( $rec->post_status );
870 $column_value = $post_status->label;
871 break;
872
873 case 'col_post_date':
874 $column_value = date_i18n( $date_format, strtotime( $rec->post_date ) );
875 break;
876
877 case 'col_row_action':
878 $column_value = sprintf(
879 '<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>',
880 $rec->ID,
881 esc_html__( 'Save', 'wordpress-seo' ),
882 esc_html__( 'Save all', 'wordpress-seo' )
883 );
884 break;
885 }
886
887 if ( ! empty( $column_value ) ) {
888 return $column_value;
889 }
890 }
891
892 /**
893 * Parse the field where the existing meta-data value is displayed.
894 *
895 * @param int $record_id Record ID.
896 * @param string $attributes HTML attributes.
897 * @param bool|array $values Optional values data array.
898 *
899 * @return string
900 */
901 protected function parse_meta_data_field( $record_id, $attributes, $values = false ) {
902
903 // Fill meta data if exists in $this->meta_data.
904 $meta_data = ( ! empty( $this->meta_data[ $record_id ] ) ) ? $this->meta_data[ $record_id ] : [];
905 $meta_key = WPSEO_Meta::$meta_prefix . $this->target_db_field;
906 $meta_value = ( ! empty( $meta_data[ $meta_key ] ) ) ? $meta_data[ $meta_key ] : '';
907
908 if ( ! empty( $values ) ) {
909 $meta_value = $values[ $meta_value ];
910 }
911
912 $id = "wpseo-existing-$this->target_db_field-$record_id";
913
914 // $attributes correctly escaped, verified by Alexander. See WPSEO_Bulk_Description_List_Table::parse_page_specific_column.
915 return sprintf( '<td %2$s id="%3$s">%1$s</td>', esc_html( $meta_value ), $attributes, esc_attr( $id ) );
916 }
917
918 /**
919 * Method for setting the meta data, which belongs to the records that will be shown on the current page.
920 *
921 * This method will loop through the current items ($this->items) for getting the post_id. With this data
922 * ($needed_ids) the method will query the meta-data table for getting the title.
923 */
924 protected function get_meta_data() {
925
926 $post_ids = $this->get_post_ids();
927 $meta_data = $this->get_meta_data_result( $post_ids );
928
929 $this->parse_meta_data( $meta_data );
930
931 // Little housekeeping.
932 unset( $post_ids, $meta_data );
933 }
934
935 /**
936 * Getting all post_ids from to $this->items.
937 *
938 * @return array
939 */
940 protected function get_post_ids() {
941 $post_ids = [];
942 foreach ( $this->items as $item ) {
943 $post_ids[] = $item->ID;
944 }
945
946 return $post_ids;
947 }
948
949 /**
950 * Getting the meta_data from database.
951 *
952 * @param array $post_ids Post IDs for SQL IN part.
953 *
954 * @return mixed
955 */
956 protected function get_meta_data_result( array $post_ids ) {
957 global $wpdb;
958
959 $where = $wpdb->prepare(
960 'post_id IN (' . implode( ', ', array_fill( 0, count( $post_ids ), '%d' ) ) . ')',
961 $post_ids
962 );
963
964 $where .= $wpdb->prepare( ' AND meta_key = %s', WPSEO_Meta::$meta_prefix . $this->target_db_field );
965
966 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- They are prepared on the lines above.
967 return $wpdb->get_results( "SELECT * FROM {$wpdb->postmeta} WHERE {$where}" );
968 }
969
970 /**
971 * Setting $this->meta_data.
972 *
973 * @param array $meta_data Meta data set.
974 */
975 protected function parse_meta_data( $meta_data ) {
976
977 foreach ( $meta_data as $row ) {
978 $this->meta_data[ $row->post_id ][ $row->meta_key ] = $row->meta_value;
979 }
980 }
981
982 /**
983 * This method will merge general array with given parameter $columns.
984 *
985 * @param array $columns Optional columns set.
986 *
987 * @return array
988 */
989 protected function merge_columns( $columns = [] ) {
990 $columns = array_merge(
991 [
992 'col_page_title' => __( 'WP Page Title', 'wordpress-seo' ),
993 'col_post_type' => __( 'Content Type', 'wordpress-seo' ),
994 'col_post_status' => __( 'Post Status', 'wordpress-seo' ),
995 'col_post_date' => __( 'Publication date', 'wordpress-seo' ),
996 'col_page_slug' => __( 'Page URL/Slug', 'wordpress-seo' ),
997 ],
998 $columns
999 );
1000
1001 $columns['col_row_action'] = __( 'Action', 'wordpress-seo' );
1002
1003 return $columns;
1004 }
1005 }
1006