analysis_seo = new WPSEO_Metabox_Analysis_SEO(); $this->analysis_readability = new WPSEO_Metabox_Analysis_Readability(); $this->admin_columns_cache = YoastSEO()->classes->get( Admin_Columns_Cache_Integration::class ); $this->score_icon_helper = YoastSEO()->helpers->score_icon; } /** * Sets up up the hooks. */ public function setup_hooks() { $this->set_post_type_hooks(); if ( $this->analysis_seo->is_enabled() ) { add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown' ] ); } if ( $this->analysis_readability->is_enabled() ) { add_action( 'restrict_manage_posts', [ $this, 'posts_filter_dropdown_readability' ] ); } add_filter( 'request', [ $this, 'column_sort_orderby' ] ); add_filter( 'default_hidden_columns', [ $this, 'column_hidden' ], 10, 1 ); } /** * Adds the column headings for the SEO plugin for edit posts / pages overview. * * @param array $columns Already existing columns. * * @return array Array containing the column headings. */ public function column_heading( $columns ) { if ( $this->display_metabox() === false ) { return $columns; } $added_columns = []; if ( $this->analysis_seo->is_enabled() ) { $added_columns['wpseo-score'] = '' . __( 'SEO score', 'wordpress-seo' ) . ''; } if ( $this->analysis_readability->is_enabled() ) { $added_columns['wpseo-score-readability'] = '' . __( 'Readability score', 'wordpress-seo' ) . ''; } $added_columns['wpseo-title'] = __( 'SEO Title', 'wordpress-seo' ); $added_columns['wpseo-metadesc'] = __( 'Meta Desc.', 'wordpress-seo' ); if ( $this->analysis_seo->is_enabled() ) { $added_columns['wpseo-focuskw'] = __( 'Keyphrase', 'wordpress-seo' ); } return array_merge( $columns, $added_columns ); } /** * Displays the column content for the given column. * * @param string $column_name Column to display the content for. * @param int $post_id Post to display the column content for. */ public function column_content( $column_name, $post_id ) { if ( $this->display_metabox() === false ) { return; } switch ( $column_name ) { case 'wpseo-score': // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method. echo $this->parse_column_score( $post_id ); return; case 'wpseo-score-readability': // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Correctly escaped in render_score_indicator() method. echo $this->parse_column_score_readability( $post_id ); return; case 'wpseo-title': $meta = $this->get_meta( $post_id ); if ( $meta ) { echo esc_html( $meta->title ); } return; case 'wpseo-metadesc': $metadesc_val = ''; $meta = $this->get_meta( $post_id ); if ( $meta ) { $metadesc_val = $meta->meta_description; } if ( $metadesc_val === '' ) { echo '', esc_html__( 'Meta description not set.', 'wordpress-seo' ), ''; return; } echo esc_html( $metadesc_val ); return; case 'wpseo-focuskw': $focuskw_val = WPSEO_Meta::get_value( 'focuskw', $post_id ); if ( $focuskw_val === '' ) { echo '', esc_html__( 'Focus keyphrase not set.', 'wordpress-seo' ), ''; return; } echo esc_html( $focuskw_val ); return; } } /** * Indicates which of the SEO columns are sortable. * * @param array $columns Appended with their orderby variable. * * @return array Array containing the sortable columns. */ public function column_sort( $columns ) { if ( $this->display_metabox() === false ) { return $columns; } $columns['wpseo-metadesc'] = 'wpseo-metadesc'; if ( $this->analysis_seo->is_enabled() ) { $columns['wpseo-focuskw'] = 'wpseo-focuskw'; $columns['wpseo-score'] = 'wpseo-score'; } if ( $this->analysis_readability->is_enabled() ) { $columns['wpseo-score-readability'] = 'wpseo-score-readability'; } return $columns; } /** * Hides the SEO title, meta description and focus keyword columns if the user hasn't chosen which columns to hide. * * @param array $hidden The hidden columns. * * @return array Array containing the columns to hide. */ public function column_hidden( $hidden ) { if ( ! is_array( $hidden ) ) { $hidden = []; } array_push( $hidden, 'wpseo-title', 'wpseo-metadesc' ); if ( $this->analysis_seo->is_enabled() ) { $hidden[] = 'wpseo-focuskw'; } return $hidden; } /** * Adds a dropdown that allows filtering on the posts SEO Quality. */ public function posts_filter_dropdown() { if ( ! $this->can_display_filter() ) { return; } $ranks = WPSEO_Rank::get_all_ranks(); echo ''; echo ''; } /** * Adds a dropdown that allows filtering on the posts Readability Quality. * * @return void */ public function posts_filter_dropdown_readability() { if ( ! $this->can_display_filter() ) { return; } $ranks = WPSEO_Rank::get_all_readability_ranks(); echo ''; echo ''; } /** * Generates an '; } /** * Returns the meta object for a given post ID. * * @param int $post_id The post ID. * * @return Meta The meta object. */ protected function get_meta( $post_id ) { $indexable = $this->admin_columns_cache->get_indexable( $post_id ); return YoastSEO()->meta->for_indexable( $indexable, 'Post_Type' ); } /** * Determines the SEO score filter to be later used in the meta query, based on the passed SEO filter. * * @param string $seo_filter The SEO filter to use to determine what further filter to apply. * * @return array The SEO score filter. */ protected function determine_seo_filters( $seo_filter ) { if ( $seo_filter === WPSEO_Rank::NO_FOCUS ) { return $this->create_no_focus_keyword_filter(); } if ( $seo_filter === WPSEO_Rank::NO_INDEX ) { return $this->create_no_index_filter(); } $rank = new WPSEO_Rank( $seo_filter ); return $this->create_seo_score_filter( $rank->get_starting_score(), $rank->get_end_score() ); } /** * Determines the Readability score filter to the meta query, based on the passed Readability filter. * * @param string $readability_filter The Readability filter to use to determine what further filter to apply. * * @return array The Readability score filter. */ protected function determine_readability_filters( $readability_filter ) { $rank = new WPSEO_Rank( $readability_filter ); return $this->create_readability_score_filter( $rank->get_starting_score(), $rank->get_end_score() ); } /** * Creates a keyword filter for the meta query, based on the passed Keyword filter. * * @param string $keyword_filter The keyword filter to use. * * @return array The keyword filter. */ protected function get_keyword_filter( $keyword_filter ) { return [ 'post_type' => get_query_var( 'post_type', 'post' ), 'key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'value' => sanitize_text_field( $keyword_filter ), ]; } /** * Determines whether the passed filter is considered to be valid. * * @param mixed $filter The filter to check against. * * @return bool Whether the filter is considered valid. */ protected function is_valid_filter( $filter ) { return ! empty( $filter ) && is_string( $filter ); } /** * Collects the filters and merges them into a single array. * * @return array Array containing all the applicable filters. */ protected function collect_filters() { $active_filters = []; $seo_filter = $this->get_current_seo_filter(); $readability_filter = $this->get_current_readability_filter(); $current_keyword_filter = $this->get_current_keyword_filter(); if ( $this->is_valid_filter( $seo_filter ) ) { $active_filters = array_merge( $active_filters, $this->determine_seo_filters( $seo_filter ) ); } if ( $this->is_valid_filter( $readability_filter ) ) { $active_filters = array_merge( $active_filters, $this->determine_readability_filters( $readability_filter ) ); } if ( $this->is_valid_filter( $current_keyword_filter ) ) { /** * Adapt the meta query used to filter the post overview on keyphrase. * * @internal * * @api array $keyword_filter The current keyword filter. * * @param array $keyphrase The keyphrase used in the filter. */ $keyphrase_filter = \apply_filters( 'wpseo_change_keyphrase_filter_in_request', $this->get_keyword_filter( $current_keyword_filter ), $current_keyword_filter ); if ( \is_array( $keyphrase_filter ) ) { $active_filters = array_merge( $active_filters, [ $keyphrase_filter ] ); } } /** * Adapt the active applicable filters on the posts overview. * * @internal * * @param array $active_filters The current applicable filters. */ return \apply_filters( 'wpseo_change_applicable_filters', $active_filters ); } /** * Modify the query based on the filters that are being passed. * * @param array $vars Query variables that need to be modified based on the filters. * * @return array Array containing the meta query to use for filtering the posts overview. */ public function column_sort_orderby( $vars ) { $collected_filters = $this->collect_filters(); $order_by_column = $vars['orderby']; if ( isset( $order_by_column ) ) { // Based on the selected column, create a meta query. $order_by = $this->filter_order_by( $order_by_column ); /** * Adapt the order by part of the query on the posts overview. * * @internal * * @param array $order_by The current order by. * @param string $order_by_column The current order by column. */ $order_by = \apply_filters( 'wpseo_change_order_by', $order_by, $order_by_column ); $vars = array_merge( $vars, $order_by ); } return $this->build_filter_query( $vars, $collected_filters ); } /** * Retrieves the meta robots query values to be used within the meta query. * * @return array Array containing the query parameters regarding meta robots. */ protected function get_meta_robots_query_values() { return [ 'relation' => 'OR', [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'compare' => 'NOT EXISTS', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'value' => '1', 'compare' => '!=', ], ]; } /** * Determines the score filters to be used. If more than one is passed, it created an AND statement for the query. * * @param array $score_filters Array containing the score filters. * * @return array Array containing the score filters that need to be applied to the meta query. */ protected function determine_score_filters( $score_filters ) { if ( count( $score_filters ) > 1 ) { return array_merge( [ 'relation' => 'AND' ], $score_filters ); } return $score_filters; } /** * Retrieves the post type from the $_GET variable. * * @return string|null The sanitized current post type or null when the variable is not set in $_GET. */ public function get_current_post_type() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['post_type'] ) && is_string( $_GET['post_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['post_type'] ) ); } return null; } /** * Retrieves the SEO filter from the $_GET variable. * * @return string|null The sanitized seo filter or null when the variable is not set in $_GET. */ public function get_current_seo_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['seo_filter'] ) && is_string( $_GET['seo_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['seo_filter'] ) ); } return null; } /** * Retrieves the Readability filter from the $_GET variable. * * @return string|null The sanitized readability filter or null when the variable is not set in $_GET. */ public function get_current_readability_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['readability_filter'] ) && is_string( $_GET['readability_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['readability_filter'] ) ); } return null; } /** * Retrieves the keyword filter from the $_GET variable. * * @return string|null The sanitized seo keyword filter or null when the variable is not set in $_GET. */ public function get_current_keyword_filter() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. if ( isset( $_GET['seo_kw_filter'] ) && is_string( $_GET['seo_kw_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reason: We are not processing form information. return sanitize_text_field( wp_unslash( $_GET['seo_kw_filter'] ) ); } return null; } /** * Uses the vars to create a complete filter query that can later be executed to filter out posts. * * @param array $vars Array containing the variables that will be used in the meta query. * @param array $filters Array containing the filters that we need to apply in the meta query. * * @return array Array containing the complete filter query. */ protected function build_filter_query( $vars, $filters ) { // If no filters were applied, just return everything. if ( count( $filters ) === 0 ) { return $vars; } $result = [ 'meta_query' => [] ]; $result['meta_query'] = array_merge( $result['meta_query'], [ $this->determine_score_filters( $filters ) ] ); $current_seo_filter = $this->get_current_seo_filter(); // This only applies for the SEO score filter because it can because the SEO score can be altered by the no-index option. if ( $this->is_valid_filter( $current_seo_filter ) && ! in_array( $current_seo_filter, [ WPSEO_Rank::NO_INDEX, WPSEO_Rank::NO_FOCUS ], true ) ) { $result['meta_query'] = array_merge( $result['meta_query'], [ $this->get_meta_robots_query_values() ] ); } return array_merge( $vars, $result ); } /** * Creates a Readability score filter. * * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * * @return array The Readability Score filter. */ protected function create_readability_score_filter( $low, $high ) { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'content_score', 'value' => [ $low, $high ], 'type' => 'numeric', 'compare' => 'BETWEEN', ], ]; } /** * Creates an SEO score filter. * * @param number $low The lower boundary of the score. * @param number $high The higher boundary of the score. * * @return array The SEO score filter. */ protected function create_seo_score_filter( $low, $high ) { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => [ $low, $high ], 'type' => 'numeric', 'compare' => 'BETWEEN', ], ]; } /** * Creates a filter to retrieve posts that were set to no-index. * * @return array Array containin the no-index filter. */ protected function create_no_index_filter() { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'value' => '1', 'compare' => '=', ], ]; } /** * Creates a filter to retrieve posts that have no keyword set. * * @return array Array containing the no focus keyword filter. */ protected function create_no_focus_keyword_filter() { return [ [ 'key' => WPSEO_Meta::$meta_prefix . 'meta-robots-noindex', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], [ 'key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'value' => 'needs-a-value-anyway', 'compare' => 'NOT EXISTS', ], ]; } /** * Determines whether a particular post_id is of an indexable post type. * * @param string $post_id The post ID to check. * * @return bool Whether or not it is indexable. */ protected function is_indexable( $post_id ) { if ( ! empty( $post_id ) && ! $this->uses_default_indexing( $post_id ) ) { return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '2'; } $post = get_post( $post_id ); if ( is_object( $post ) ) { // If the option is false, this means we want to index it. return WPSEO_Options::get( 'noindex-' . $post->post_type, false ) === false; } return true; } /** * Determines whether the given post ID uses the default indexing settings. * * @param int $post_id The post ID to check. * * @return bool Whether or not the default indexing is being used for the post. */ protected function uses_default_indexing( $post_id ) { return WPSEO_Meta::get_value( 'meta-robots-noindex', $post_id ) === '0'; } /** * Returns filters when $order_by is matched in the if-statement. * * @param string $order_by The ID of the column by which to order the posts. * * @return array Array containing the order filters. */ private function filter_order_by( $order_by ) { switch ( $order_by ) { case 'wpseo-metadesc': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'metadesc', 'orderby' => 'meta_value', ]; case 'wpseo-focuskw': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'focuskw', 'orderby' => 'meta_value', ]; case 'wpseo-score': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'linkdex', 'orderby' => 'meta_value_num', ]; case 'wpseo-score-readability': return [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Reason: Only used when user requests sorting. 'meta_key' => WPSEO_Meta::$meta_prefix . 'content_score', 'orderby' => 'meta_value_num', ]; } return []; } /** * Parses the score column. * * @param int $post_id The ID of the post for which to show the score. * * @return string The HTML for the SEO score indicator. */ private function parse_column_score( $post_id ) { $meta = $this->get_meta( $post_id ); if ( $meta ) { return $this->score_icon_helper->for_seo( $meta->indexable, '', __( 'Post is set to noindex.', 'wordpress-seo' ) ); } } /** * Parsing the readability score column. * * @param int $post_id The ID of the post for which to show the readability score. * * @return string The HTML for the readability score indicator. */ private function parse_column_score_readability( $post_id ) { $meta = $this->get_meta( $post_id ); if ( $meta ) { return $this->score_icon_helper->for_readability( $meta->indexable->readability_score ); } } /** * Sets up the hooks for the post_types. */ private function set_post_type_hooks() { $post_types = WPSEO_Post_Type::get_accessible_post_types(); if ( ! is_array( $post_types ) || $post_types === [] ) { return; } foreach ( $post_types as $post_type ) { if ( $this->display_metabox( $post_type ) === false ) { continue; } add_filter( 'manage_' . $post_type . '_posts_columns', [ $this, 'column_heading' ], 10, 1 ); add_action( 'manage_' . $post_type . '_posts_custom_column', [ $this, 'column_content' ], 10, 2 ); add_action( 'manage_edit-' . $post_type . '_sortable_columns', [ $this, 'column_sort' ], 10, 2 ); } unset( $post_type ); } /** * Wraps the WPSEO_Metabox check to determine whether the metabox should be displayed either by * choice of the admin or because the post type is not a public post type. * * @since 7.0 * * @param string|null $post_type Optional. The post type to test, defaults to the current post post_type. * * @return bool Whether or not the meta box (and associated columns etc) should be hidden. */ private function display_metabox( $post_type = null ) { $current_post_type = $this->get_current_post_type(); if ( ! isset( $post_type ) && ! empty( $current_post_type ) ) { $post_type = $current_post_type; } return WPSEO_Utils::is_metabox_active( $post_type, 'post_type' ); } /** * Determines whether or not filter dropdowns should be displayed. * * @return bool Whether or the current page can display the filter drop downs. */ public function can_display_filter() { if ( $GLOBALS['pagenow'] === 'upload.php' ) { return false; } if ( $this->display_metabox() === false ) { return false; } $screen = get_current_screen(); if ( $screen === null ) { return false; } return WPSEO_Post_Type::is_post_type_accessible( $screen->post_type ); } }