PluginProbe ʕ •ᴥ•ʔ
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.5
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.5
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-meta-columns.php
wordpress-seo / admin Last commit date
ajax 2 years ago capabilities 1 year ago endpoints 2 years ago exceptions 3 months ago filters 3 months ago formatter 1 year ago google_search_console 3 months ago import 3 months ago listeners 8 years ago menu 3 months ago metabox 3 months ago notifiers 3 months ago pages 3 months ago roles 3 months ago services 3 months ago statistics 3 months ago taxonomy 3 months ago tracking 3 months ago views 3 months ago watchers 3 months ago admin-settings-changed-listener.php 2 years ago ajax.php 3 months ago class-admin-asset-analysis-worker-location.php 3 months ago class-admin-asset-dev-server-location.php 3 months ago class-admin-asset-location.php 8 years ago class-admin-asset-manager.php 3 months ago class-admin-asset-seo-location.php 4 years ago class-admin-editor-specific-replace-vars.php 3 months ago class-admin-gutenberg-compatibility-notification.php 3 months ago class-admin-help-panel.php 3 months ago class-admin-init.php 3 months ago class-admin-recommended-replace-vars.php 2 years ago class-admin-user-profile.php 7 months ago class-admin-utils.php 3 months ago class-admin.php 3 months ago class-asset.php 1 year ago class-bulk-description-editor-list-table.php 3 months ago class-bulk-editor-list-table.php 3 months ago class-bulk-title-editor-list-table.php 3 months ago class-collector.php 1 year ago class-config.php 3 months ago class-database-proxy.php 3 months ago class-export.php 3 months ago class-expose-shortlinks.php 7 months ago class-gutenberg-compatibility.php 1 month ago class-meta-columns.php 3 months ago class-my-yoast-proxy.php 3 months ago class-option-tab.php 4 years ago class-option-tabs-formatter.php 3 months ago class-option-tabs.php 2 years ago class-paper-presenter.php 5 years ago class-plugin-availability.php 3 months ago class-plugin-conflict.php 2 years ago class-premium-popup.php 1 year ago class-premium-upsell-admin-block.php 3 months ago class-primary-term-admin.php 3 months ago class-product-upsell-notice.php 3 months ago class-remote-request.php 2 years ago class-schema-person-upgrade-notification.php 3 months ago class-suggested-plugins.php 3 months ago class-wincher-dashboard-widget.php 3 months ago class-yoast-columns.php 3 months ago class-yoast-dashboard-widget.php 3 months ago class-yoast-form.php 3 months ago class-yoast-input-validation.php 3 months ago class-yoast-network-admin.php 3 months ago class-yoast-network-settings-api.php 3 months ago class-yoast-notification-center.php 3 months ago class-yoast-notification.php 3 months ago class-yoast-notifications.php 3 months ago class-yoast-plugin-conflict.php 3 months ago index.php 10 years ago interface-collection.php 7 years ago interface-installable.php 8 years ago
class-meta-columns.php
913 lines
1 <?php
2 /**
3 * WPSEO plugin file.
4 *
5 * @package WPSEO\Admin
6 */
7
8 use Yoast\WP\SEO\Context\Meta_Tags_Context;
9 use Yoast\WP\SEO\Helpers\Score_Icon_Helper;
10 use Yoast\WP\SEO\Integrations\Admin\Admin_Columns_Cache_Integration;
11 use Yoast\WP\SEO\Surfaces\Values\Meta;
12
13 /**
14 * Class WPSEO_Meta_Columns.
15 */
16 class WPSEO_Meta_Columns {
17
18 /**
19 * Holds the context objects for each indexable.
20 *
21 * @var Meta_Tags_Context[]
22 */
23 protected $context = [];
24
25 /**
26 * Holds the SEO analysis.
27 *
28 * @var WPSEO_Metabox_Analysis_SEO
29 */
30 private $analysis_seo;
31
32 /**
33 * Holds the readability analysis.
34 *
35 * @var WPSEO_Metabox_Analysis_Readability
36 */
37 private $analysis_readability;
38
39 /**
40 * Admin columns cache.
41 *
42 * @var Admin_Columns_Cache_Integration
43 */
44 private $admin_columns_cache;
45
46 /**
47 * Holds the Score_Icon_Helper.
48 *
49 * @var Score_Icon_Helper
50 */
51 private $score_icon_helper;
52
53 /**
54 * Holds the WPSEO_Admin_Asset_Manager instance.
55 *
56 * @var WPSEO_Admin_Asset_Manager
57 */
58 private $admin_asset_manager;
59
60 /**
61 * When page analysis is enabled, just initialize the hooks.
62 */
63 public function __construct() {
64 if ( apply_filters( 'wpseo_use_page_analysis', true ) === true ) {
65 add_action( 'admin_init', [ $this, 'setup_hooks' ] );
66 }
67
68 $this->analysis_seo = new WPSEO_Metabox_Analysis_SEO();
69 $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability();
70 $this->admin_columns_cache = YoastSEO()->classes->get( Admin_Columns_Cache_Integration::class );
71 $this->score_icon_helper = YoastSEO()->helpers->score_icon;
72 $this->admin_asset_manager = YoastSEO()->classes->get( WPSEO_Admin_Asset_Manager::class );
73 }
74
75 /**
76 * Sets up up the hooks.
77 *
78 * @return void
79 */
80 public function setup_hooks() {
81 $this->set_post_type_hooks();
82
83 if ( $this->analysis_seo->is_enabled() ) {
84 add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown' ] );
85 }
86
87 if ( $this->analysis_readability->is_enabled() ) {
88 add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown_readability' ] );
89 }
90
91 add_filter( 'request', [ $this, 'column_sort_orderby' ] );
92 add_filter( 'default_hidden_columns', [ $this, 'column_hidden' ], 10, 1 );
93 }
94
95 /**
96 * Adds the column headings for the SEO plugin for edit posts / pages overview.
97 *
98 * @param array $columns Already existing columns.
99 *
100 * @return array Array containing the column headings.
101 */
102 public function column_heading( $columns ) {
103 if ( $this->display_metabox() === false ) {
104 return $columns;
105 }
106
107 $this->admin_asset_manager->enqueue_script( 'edit-page' );
108 $this->admin_asset_manager->enqueue_style( 'edit-page' );
109
110 $added_columns = [];
111
112 if ( $this->analysis_seo->is_enabled() ) {
113 $added_columns['wpseo-score'] = '<span class="yoast-column-seo-score yoast-column-header-has-tooltip" data-tooltip-text="'
114 . esc_attr__( 'SEO score', 'wordpress-seo' )
115 . '"><span class="screen-reader-text">'
116 . __( 'SEO score', 'wordpress-seo' )
117 . '</span></span>';
118 }
119
120 if ( $this->analysis_readability->is_enabled() ) {
121 $added_columns['wpseo-score-readability'] = '<span class="yoast-column-readability yoast-column-header-has-tooltip" data-tooltip-text="'
122 . esc_attr__( 'Readability score', 'wordpress-seo' )
123 . '"><span class="screen-reader-text">'
124 . __( 'Readability score', 'wordpress-seo' )
125 . '</span></span>';
126 }
127
128 $added_columns['wpseo-title'] = __( 'SEO Title', 'wordpress-seo' );
129 $added_columns['wpseo-metadesc'] = __( 'Meta Desc.', 'wordpress-seo' );
130
131 if ( $this->analysis_seo->is_enabled() ) {
132 $added_columns['wpseo-focuskw'] = __( 'Keyphrase', 'wordpress-seo' );
133 }
134
135 return array_merge( $columns, $added_columns );
136 }
137
138 /**
139 * Displays the column content for the given column.
140 *
141 * @param string $column_name Column to display the content for.
142 * @param int $post_id Post to display the column content for.
143 *
144 * @return void
145 */
146 public function column_content( $column_name, $post_id ) {
147 if ( $this->display_metabox() === false ) {
148 return;
149 }
150
151 switch ( $column_name ) {
152 case 'wpseo-score':
153 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method.
154 echo $this->parse_column_score( $post_id );
155
156 return;
157
158 case 'wpseo-score-readability':
159 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method.
160 echo $this->parse_column_score_readability( $post_id );
161
162 return;
163
164 case 'wpseo-title':
165 $meta = $this->get_meta( $post_id );
166 if ( $meta ) {
167 echo esc_html( $meta->title );
168 }
169
170 return;
171
172 case 'wpseo-metadesc':
173 $metadesc_val = '';
174 $meta = $this->get_meta( $post_id );
175 if ( $meta ) {
176 $metadesc_val = $meta->meta_description;
177 }
178 if ( $metadesc_val === '' ) {
179 echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">',
180 /* translators: Hidden accessibility text. */
181 esc_html__( 'Meta description not set.', 'wordpress-seo' ),
182 '</span>';
183
184 return;
185 }
186
187 echo esc_html( $metadesc_val );
188
189 return;
190
191 case 'wpseo-focuskw':
192 $focuskw_val = WPSEO_Meta::get_value( 'focuskw', $post_id );
193
194 if ( $focuskw_val === '' ) {
195 echo '<span aria-hidden="true">&#8212;</span><span class="screen-reader-text">',
196 /* translators: Hidden accessibility text. */
197 esc_html__( 'Focus keyphrase not set.', 'wordpress-seo' ),
198 '</span>';
199
200 return;
201 }
202
203 echo esc_html( $focuskw_val );
204
205 return;
206 }
207 }
208
209 /**
210 * Indicates which of the SEO columns are sortable.
211 *
212 * @param array $columns Appended with their orderby variable.
213 *
214 * @return array Array containing the sortable columns.
215 */
216 public function column_sort( $columns ) {
217 if ( $this->display_metabox() === false ) {
218 return $columns;
219 }
220
221 $columns['wpseo-metadesc'] = 'wpseo-metadesc';
222
223 if ( $this->analysis_seo->is_enabled() ) {
224 $columns['wpseo-focuskw'] = 'wpseo-focuskw';
225 $columns['wpseo-score'] = 'wpseo-score';
226 }
227
228 if ( $this->analysis_readability->is_enabled() ) {
229 $columns['wpseo-score-readability'] = 'wpseo-score-readability';
230 }
231
232 return $columns;
233 }
234
235 /**
236 * Hides the SEO title, meta description and focus keyword columns if the user hasn't chosen which columns to hide.
237 *
238 * @param array $hidden The hidden columns.
239 *
240 * @return array Array containing the columns to hide.
241 */
242 public function column_hidden( $hidden ) {
243 if ( ! is_array( $hidden ) ) {
244 $hidden = [];
245 }
246
247 array_push( $hidden, 'wpseo-title', 'wpseo-metadesc' );
248
249 if ( $this->analysis_seo->is_enabled() ) {
250 $hidden[] = 'wpseo-focuskw';
251 }
252
253 return $hidden;
254 }
255
256 /**
257 * Adds a dropdown that allows filtering on the posts SEO Quality.
258 *
259 * @return void
260 */
261 public function posts_filter_dropdown() {
262 if ( ! $this->can_display_filter() ) {
263 return;
264 }
265
266 $ranks = WPSEO_Rank::get_all_ranks();
267
268 /* translators: Hidden accessibility text. */
269 echo '<label class="screen-reader-text" for="wpseo-filter">' . esc_html__( 'Filter by SEO Score', 'wordpress-seo' ) . '</label>';
270 echo '<select name="seo_filter" id="wpseo-filter">';
271
272 // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
273 echo $this->generate_option( '', __( 'All SEO Scores', 'wordpress-seo' ) );
274
275 foreach ( $ranks as $rank ) {
276 $selected = selected( $this->get_current_seo_filter(), $rank->get_rank(), false );
277
278 // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
279 echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_label(), $selected );
280 }
281
282 echo '</select>';
283 }
284
285 /**
286 * Adds a dropdown that allows filtering on the posts Readability Quality.
287 *
288 * @return void
289 */
290 public function posts_filter_dropdown_readability() {
291 if ( ! $this->can_display_filter() ) {
292 return;
293 }
294
295 $ranks = WPSEO_Rank::get_all_readability_ranks();
296
297 /* translators: Hidden accessibility text. */
298 echo '<label class="screen-reader-text" for="wpseo-readability-filter">' . esc_html__( 'Filter by Readability Score', 'wordpress-seo' ) . '</label>';
299 echo '<select name="readability_filter" id="wpseo-readability-filter">';
300
301 // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
302 echo $this->generate_option( '', __( 'All Readability Scores', 'wordpress-seo' ) );
303
304 foreach ( $ranks as $rank ) {
305 $selected = selected( $this->get_current_readability_filter(), $rank->get_rank(), false );
306
307 // phpcs:ignore WordPress.Security.EscapeOutput -- Output is correctly escaped in the generate_option() method.
308 echo $this->generate_option( $rank->get_rank(), $rank->get_drop_down_readability_labels(), $selected );
309 }
310
311 echo '</select>';
312 }
313
314 /**
315 * Generates an <option> element.
316 *
317 * @param string $value The option's value.
318 * @param string $label The option's label.
319 * @param string $selected HTML selected attribute for an option.
320 *
321 * @return string The generated <option> element.
322 */
323 protected function generate_option( $value, $label, $selected = '' ) {
324 return '<option ' . $selected . ' value="' . esc_attr( $value ) . '">' . esc_html( $label ) . '</option>';
325 }
326
327 /**
328 * Returns the meta object for a given post ID.
329 *
330 * @param int $post_id The post ID.
331 *
332 * @return Meta The meta object.
333 */
334 protected function get_meta( $post_id ) {
335 $indexable = $this->admin_columns_cache->get_indexable( $post_id );
336
337 return YoastSEO()->meta->for_indexable( $indexable, 'Post_Type' );
338 }
339
340 /**
341 * Determines the SEO score filter to be later used in the meta query, based on the passed SEO filter.
342 *
343 * @param string $seo_filter The SEO filter to use to determine what further filter to apply.
344 *
345 * @return array The SEO score filter.
346 */
347 protected function determine_seo_filters( $seo_filter ) {
348 if ( $seo_filter === WPSEO_Rank::NO_FOCUS ) {
349 return $this->create_no_focus_keyword_filter();
350 }
351
352 if ( $seo_filter === WPSEO_Rank::NO_INDEX ) {
353 return $this->create_no_index_filter();
354 }
355
356 $rank = new WPSEO_Rank( $seo_filter );
357
358 return $this->create_seo_score_filter( $rank->get_starting_score(), $rank->get_end_score() );
359 }
360
361 /**
362 * Determines the Readability score filter to the meta query, based on the passed Readability filter.
363 *
364 * @param string $readability_filter The Readability filter to use to determine what further filter to apply.
365 *
366 * @return array The Readability score filter.
367 */
368 protected function determine_readability_filters( $readability_filter ) {
369 if ( $readability_filter === WPSEO_Rank::NO_FOCUS ) {
370 return $this->create_no_readability_scores_filter();
371 }
372 if ( $readability_filter === WPSEO_Rank::BAD ) {
373 return $this->create_bad_readability_scores_filter();
374 }
375 $rank = new WPSEO_Rank( $readability_filter );
376
377 return $this->create_readability_score_filter( $rank->get_starting_score(), $rank->get_end_score() );
378 }
379
380 /**
381 * Creates a keyword filter for the meta query, based on the passed Keyword filter.
382 *
383 * @param string $keyword_filter The keyword filter to use.
384 *
385 * @return array The keyword filter.
386 */
387 protected function get_keyword_filter( $keyword_filter ) {
388 return [
389 'post_type' => get_query_var( 'post_type', 'post' ),
390 'key' => WPSEO_Meta::$meta_prefix . 'focuskw',
391 'value' => sanitize_text_field( $keyword_filter ),
392 ];
393 }
394
395 /**
396 * Determines whether the passed filter is considered to be valid.
397 *
398 * @param mixed $filter The filter to check against.
399 *
400 * @return bool Whether the filter is considered valid.
401 */
402 protected function is_valid_filter( $filter ) {
403 return ! empty( $filter ) && is_string( $filter );
404 }
405
406 /**
407 * Collects the filters and merges them into a single array.
408 *
409 * @return array Array containing all the applicable filters.
410 */
411 protected function collect_filters() {
412 $active_filters = [];
413
414 $seo_filter = $this->get_current_seo_filter();
415 $readability_filter = $this->get_current_readability_filter();
416 $current_keyword_filter = $this->get_current_keyword_filter();
417
418 if ( $this->is_valid_filter( $seo_filter ) ) {
419 $active_filters = array_merge(
420 $active_filters,
421 $this->determine_seo_filters( $seo_filter ),
422 );
423 }
424
425 if ( $this->is_valid_filter( $readability_filter ) ) {
426 $active_filters = array_merge(
427 $active_filters,
428 $this->determine_readability_filters( $readability_filter ),
429 );
430 }
431
432 if ( $this->is_valid_filter( $current_keyword_filter ) ) {
433 /**
434 * Adapt the meta query used to filter the post overview on keyphrase.
435 *
436 * @internal
437 *
438 * @param array $keyphrase The keyphrase used in the filter.
439 * @param array $keyword_filter The current keyword filter.
440 */
441 $keyphrase_filter = apply_filters(
442 'wpseo_change_keyphrase_filter_in_request',
443 $this->get_keyword_filter( $current_keyword_filter ),
444 $current_keyword_filter,
445 );
446
447 if ( is_array( $keyphrase_filter ) ) {
448 $active_filters = array_merge(
449 $active_filters,
450 [ $keyphrase_filter ],
451 );
452 }
453 }
454
455 /**
456 * Adapt the active applicable filters on the posts overview.
457 *
458 * @internal
459 *
460 * @param array $active_filters The current applicable filters.
461 */
462 return apply_filters( 'wpseo_change_applicable_filters', $active_filters );
463 }
464
465 /**
466 * Modify the query based on the filters that are being passed.
467 *
468 * @param array $vars Query variables that need to be modified based on the filters.
469 *
470 * @return array Array containing the meta query to use for filtering the posts overview.
471 */
472 public function column_sort_orderby( $vars ) {
473 $collected_filters = $this->collect_filters();
474
475 $order_by_column = $vars['orderby'];
476 if ( isset( $order_by_column ) ) {
477 // Based on the selected column, create a meta query.
478 $order_by = $this->filter_order_by( $order_by_column );
479
480 /**
481 * Adapt the order by part of the query on the posts overview.
482 *
483 * @internal
484 *
485 * @param array $order_by The current order by.
486 * @param string $order_by_column The current order by column.
487 */
488 $order_by = apply_filters( 'wpseo_change_order_by', $order_by, $order_by_column );
489
490 $vars = array_merge( $vars, $order_by );
491 }
492
493 return $this->build_filter_query( $vars, $collected_filters );
494 }
495
496 /**
497 * Retrieves the meta robots query values to be used within the meta query.
498 *
499 * @return array Array containing the query parameters regarding meta robots.
500 */
501 protected function get_meta_robots_query_values() {
502 return [
503 'relation' => 'OR',
504 [
505 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
506 'compare' => 'NOT EXISTS',
507 ],
508 [
509 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
510 'value' => '1',
511 'compare' => '!=',
512 ],
513 ];
514 }
515
516 /**
517 * Determines the score filters to be used. If more than one is passed, it created an AND statement for the query.
518 *
519 * @param array $score_filters Array containing the score filters.
520 *
521 * @return array Array containing the score filters that need to be applied to the meta query.
522 */
523 protected function determine_score_filters( $score_filters ) {
524 if ( count( $score_filters ) > 1 ) {
525 return array_merge( [ 'relation' => 'AND' ], $score_filters );
526 }
527
528 return $score_filters;
529 }
530
531 /**
532 * Retrieves the post type from the $_GET variable.
533 *
534 * @return string|null The sanitized current post type or null when the variable is not set in $_GET.
535 */
536 public function get_current_post_type() {
537 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
538 if ( isset( $_GET['post_type'] ) && is_string( $_GET['post_type'] ) ) {
539 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
540 return sanitize_text_field( wp_unslash( $_GET['post_type'] ) );
541 }
542 return null;
543 }
544
545 /**
546 * Retrieves the SEO filter from the $_GET variable.
547 *
548 * @return string|null The sanitized seo filter or null when the variable is not set in $_GET.
549 */
550 public function get_current_seo_filter() {
551 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
552 if ( isset( $_GET['seo_filter'] ) && is_string( $_GET['seo_filter'] ) ) {
553 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
554 return sanitize_text_field( wp_unslash( $_GET['seo_filter'] ) );
555 }
556 return null;
557 }
558
559 /**
560 * Retrieves the Readability filter from the $_GET variable.
561 *
562 * @return string|null The sanitized readability filter or null when the variable is not set in $_GET.
563 */
564 public function get_current_readability_filter() {
565 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
566 if ( isset( $_GET['readability_filter'] ) && is_string( $_GET['readability_filter'] ) ) {
567 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
568 return sanitize_text_field( wp_unslash( $_GET['readability_filter'] ) );
569 }
570 return null;
571 }
572
573 /**
574 * Retrieves the keyword filter from the $_GET variable.
575 *
576 * @return string|null The sanitized seo keyword filter or null when the variable is not set in $_GET.
577 */
578 public function get_current_keyword_filter() {
579 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
580 if ( isset( $_GET['seo_kw_filter'] ) && is_string( $_GET['seo_kw_filter'] ) ) {
581 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information.
582 return sanitize_text_field( wp_unslash( $_GET['seo_kw_filter'] ) );
583 }
584 return null;
585 }
586
587 /**
588 * Uses the vars to create a complete filter query that can later be executed to filter out posts.
589 *
590 * @param array $vars Array containing the variables that will be used in the meta query.
591 * @param array $filters Array containing the filters that we need to apply in the meta query.
592 *
593 * @return array Array containing the complete filter query.
594 */
595 protected function build_filter_query( $vars, $filters ) {
596 // If no filters were applied, just return everything.
597 if ( count( $filters ) === 0 ) {
598 return $vars;
599 }
600
601 $result = [ 'meta_query' => [] ];
602 $result['meta_query'] = array_merge( $result['meta_query'], [ $this->determine_score_filters( $filters ) ] );
603
604 $current_seo_filter = $this->get_current_seo_filter();
605
606 // This only applies for the SEO score filter because it can because the SEO score can be altered by the no-index option.
607 if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, [ WPSEO_Rank::NO_INDEX ], true ) ) {
608 $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] );
609 }
610
611 return array_merge( $vars, $result );
612 }
613
614 /**
615 * Creates a Readability score filter.
616 *
617 * @param number $low The lower boundary of the score.
618 * @param number $high The higher boundary of the score.
619 *
620 * @return array<array<string>> The Readability Score filter.
621 */
622 protected function create_readability_score_filter( $low, $high ) {
623 return [
624 [
625 'key' => WPSEO_Meta::$meta_prefix . 'content_score',
626 'value' => [ $low, $high ],
627 'type' => 'numeric',
628 'compare' => 'BETWEEN',
629 ],
630 ];
631 }
632
633 /**
634 * Creates an SEO score filter.
635 *
636 * @param number $low The lower boundary of the score.
637 * @param number $high The higher boundary of the score.
638 *
639 * @return array<array<string>> The SEO score filter.
640 */
641 protected function create_seo_score_filter( $low, $high ) {
642 return [
643 [
644 'key' => WPSEO_Meta::$meta_prefix . 'linkdex',
645 'value' => [ $low, $high ],
646 'type' => 'numeric',
647 'compare' => 'BETWEEN',
648 ],
649 ];
650 }
651
652 /**
653 * Creates a filter to retrieve posts that were set to no-index.
654 *
655 * @return array<array<string>> Array containin the no-index filter.
656 */
657 protected function create_no_index_filter() {
658 return [
659 [
660 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
661 'value' => '1',
662 'compare' => '=',
663 ],
664 ];
665 }
666
667 /**
668 * Creates a filter to retrieve posts that have no keyword set.
669 *
670 * @return array<array<string>> Array containing the no focus keyword filter.
671 */
672 protected function create_no_focus_keyword_filter() {
673 return [
674 [
675 'key' => WPSEO_Meta::$meta_prefix . 'linkdex',
676 'value' => 'needs-a-value-anyway',
677 'compare' => 'NOT EXISTS',
678 ],
679 ];
680 }
681
682 /**
683 * Creates a filter to retrieve posts that have not been analyzed for readability yet.
684 *
685 * @return array<array<string>> Array containing the no readability filter.
686 */
687 protected function create_no_readability_scores_filter() {
688 // We check the existence of the Estimated Reading Time, because readability scores of posts that haven't been manually saved while Yoast SEO is active, don't exist, which is also the case for posts with not enough content.
689 // Meanwhile, the ERT is a solid indicator of whether a post has ever been saved (aka, analyzed), so we're using that.
690 $rank = new WPSEO_Rank( WPSEO_Rank::BAD );
691 return [
692 [
693 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes',
694 'value' => 'needs-a-value-anyway',
695 'compare' => 'NOT EXISTS',
696 ],
697 [
698 'relation' => 'OR',
699 [
700 'key' => WPSEO_Meta::$meta_prefix . 'content_score',
701 'value' => $rank->get_starting_score(),
702 'type' => 'numeric',
703 'compare' => '<',
704 ],
705 [
706 'key' => WPSEO_Meta::$meta_prefix . 'content_score',
707 'value' => 'needs-a-value-anyway',
708 'compare' => 'NOT EXISTS',
709 ],
710 ],
711 ];
712 }
713
714 /**
715 * Creates a filter to retrieve posts that have bad readability scores, including those that have not enough content to have one.
716 *
717 * @return array<array<string>> Array containing the bad readability filter.
718 */
719 protected function create_bad_readability_scores_filter() {
720 $rank = new WPSEO_Rank( WPSEO_Rank::BAD );
721 return [
722 'relation' => 'OR',
723 [
724 'key' => WPSEO_Meta::$meta_prefix . 'content_score',
725 'value' => [ $rank->get_starting_score(), $rank->get_end_score() ],
726 'type' => 'numeric',
727 'compare' => 'BETWEEN',
728 ],
729 [
730 [
731 'key' => WPSEO_Meta::$meta_prefix . 'content_score',
732 'value' => 'needs-a-value-anyway',
733 'compare' => 'NOT EXISTS',
734 ],
735 [
736 'key' => WPSEO_Meta::$meta_prefix . 'estimated-reading-time-minutes',
737 'compare' => 'EXISTS',
738 ],
739 ],
740 ];
741 }
742
743 /**
744 * Determines whether a particular post_id is of an indexable post type.
745 *
746 * @param string $post_id The post ID to check.
747 *
748 * @return bool Whether or not it is indexable.
749 */
750 protected function is_indexable( $post_id ) {
751 if ( ! empty( $post_id ) && ! $this->uses_default_indexing( $post_id ) ) {
752 return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '2';
753 }
754
755 $post = get_post( $post_id );
756
757 if ( is_object( $post ) ) {
758 // If the option is false, this means we want to index it.
759 return WPSEO_Options::get( 'noindex-' . $post->post_type, false ) === false;
760 }
761
762 return true;
763 }
764
765 /**
766 * Determines whether the given post ID uses the default indexing settings.
767 *
768 * @param int $post_id The post ID to check.
769 *
770 * @return bool Whether or not the default indexing is being used for the post.
771 */
772 protected function uses_default_indexing( $post_id ) {
773 return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '0';
774 }
775
776 /**
777 * Returns filters when $order_by is matched in the if-statement.
778 *
779 * @param string $order_by The ID of the column by which to order the posts.
780 *
781 * @return array<string> Array containing the order filters.
782 */
783 private function filter_order_by( $order_by ) {
784 switch ( $order_by ) {
785 case 'wpseo-metadesc':
786 return [
787 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting.
788 'meta_key' => WPSEO_Meta::$meta_prefix . 'metadesc',
789 'orderby' => 'meta_value',
790 ];
791
792 case 'wpseo-focuskw':
793 return [
794 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting.
795 'meta_key' => WPSEO_Meta::$meta_prefix . 'focuskw',
796 'orderby' => 'meta_value',
797 ];
798
799 case 'wpseo-score':
800 return [
801 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting.
802 'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex',
803 'orderby' => 'meta_value_num',
804 ];
805
806 case 'wpseo-score-readability':
807 return [
808 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting.
809 'meta_key' => WPSEO_Meta::$meta_prefix . 'content_score',
810 'orderby' => 'meta_value_num',
811 ];
812 }
813
814 return [];
815 }
816
817 /**
818 * Parses the score column.
819 *
820 * @param int $post_id The ID of the post for which to show the score.
821 *
822 * @return string The HTML for the SEO score indicator.
823 */
824 private function parse_column_score( $post_id ) {
825 $meta = $this->get_meta( $post_id );
826
827 if ( $meta ) {
828 return $this->score_icon_helper->for_seo( $meta->indexable, '', __( 'Post is set to noindex.', 'wordpress-seo' ) );
829 }
830 }
831
832 /**
833 * Parsing the readability score column.
834 *
835 * @param int $post_id The ID of the post for which to show the readability score.
836 *
837 * @return string The HTML for the readability score indicator.
838 */
839 private function parse_column_score_readability( $post_id ) {
840 $meta = $this->get_meta( $post_id );
841 if ( $meta ) {
842 return $this->score_icon_helper->for_readability( $meta->indexable->readability_score );
843 }
844 }
845
846 /**
847 * Sets up the hooks for the post_types.
848 *
849 * @return void
850 */
851 private function set_post_type_hooks() {
852 $post_types = WPSEO_Post_Type::get_accessible_post_types();
853
854 if ( ! is_array( $post_types ) || $post_types === [] ) {
855 return;
856 }
857
858 foreach ( $post_types as $post_type ) {
859 if ( $this->display_metabox( $post_type ) === false ) {
860 continue;
861 }
862
863 add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'column_heading' ], 10, 1 );
864 add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 );
865 add_action( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ], 10, 2 );
866 }
867
868 unset( $post_type );
869 }
870
871 /**
872 * Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by
873 * choice of the admin or because the post type is not a public post type.
874 *
875 * @since 7.0
876 *
877 * @param string|null $post_type Optional. The post type to test, defaults to the current post post_type.
878 *
879 * @return bool Whether or not the meta box (and associated columns etc) should be hidden.
880 */
881 private function display_metabox( $post_type = null ) {
882 $current_post_type = $this->get_current_post_type();
883
884 if ( ! isset( $post_type ) && ! empty( $current_post_type ) ) {
885 $post_type = $current_post_type;
886 }
887
888 return WPSEO_Utils::is_metabox_active( $post_type, 'post_type' );
889 }
890
891 /**
892 * Determines whether or not filter dropdowns should be displayed.
893 *
894 * @return bool Whether or the current page can display the filter drop downs.
895 */
896 public function can_display_filter() {
897 if ( $GLOBALS['pagenow'] === 'upload.php' ) {
898 return false;
899 }
900
901 if ( $this->display_metabox() === false ) {
902 return false;
903 }
904
905 $screen = get_current_screen();
906 if ( $screen === null ) {
907 return false;
908 }
909
910 return WPSEO_Post_Type::is_post_type_accessible( $screen->post_type );
911 }
912 }
913