Your IP : 216.73.216.122


Current Path : /home/ereika83/www/wp-content/plugins/wpmu-dev-seo/includes/core/controllers/
Upload File :
Current File : /home/ereika83/www/wp-content/plugins/wpmu-dev-seo/includes/core/controllers/class-analysis.php

<?php
/**
 * Analysis (SEO and Readability) wiring hub.
 *
 * @package SmartCrawl
 */

namespace SmartCrawl\Controllers;

use SmartCrawl\Checks;
use SmartCrawl\Settings;
use SmartCrawl\Singleton;
use SmartCrawl\Simple_Renderer;
use SmartCrawl\Cache\Post_Cache;
use SmartCrawl\Models\Analysis as Analysis_Model;

/**
 * Analysis controller
 *
 * At the same time, some rendering duties.
 */
class Analysis extends Controller {

	use Singleton;

	/**
	 * Bind listening actions
	 *
	 * @return bool
	 */
	protected function init() {
		// Fetch analysis data via AJAX POST request.
		add_action( 'wp_ajax_wds-analysis-get-data', array( $this, 'retrieve_post_analysis_data' ) );
		add_action( 'wp_ajax_wds-analysis-get-markup', array( $this, 'retrieve_post_analysis_markup' ) );
		add_action( 'wp_ajax_wds-analysis-recheck', array( $this, 'retrieve_post_analysis_recheck' ) );
		add_action( 'wp_ajax_wds_analysis_get_editor_analysis', array( $this, 'retrieve_post_analysis' ) );
		add_action( 'wp_ajax_wds_analysis_add_keyword', array( $this, 'add_post_keyword' ) );
		add_action( 'wp_ajax_wds_analysis_remove_keyword', array( $this, 'remove_post_keyword' ) );
		add_action( 'wp_ajax_wds_analysis_ignore_check', array( $this, 'json_set_ignore_check' ) );
		add_action( 'wp_ajax_wds_analysis_unignore_check', array( $this, 'json_unset_ignore_check' ) );

		// Set up CPT columns filtering.
		add_action( 'admin_init', array( $this, 'set_up_post_columns' ) );

		add_action( 'parse_query', array( $this, 'apply_analysis_post_list_filter' ) );

		add_action( 'post_submitbox_misc_actions', array( $this, 'add_postbox_fields' ) );

		add_action( 'admin_enqueue_scripts', array( $this, 'inject_script_dependencies' ) );

		return true;
	}

	/**
	 * Unbinds listening actions
	 *
	 * @return bool
	 */
	protected function terminate() {
		remove_action( 'wp_ajax_wds-analysis-get-data', array( $this, 'retrieve_post_analysis_data' ) );
		remove_action( 'wp_ajax_wds-analysis-get-markup', array( $this, 'retrieve_post_analysis_markup' ) );
		remove_action( 'wp_ajax_wds-analysis-recheck', array( $this, 'retrieve_post_analysis_recheck' ) );
		remove_action( 'wp_ajax_wds_analysis_get_editor_analysis', array( $this, 'retrieve_post_analysis' ) );
		remove_action( 'wp_ajax_wds_analysis_add_keyword', array( $this, 'add_post_keyword' ) );
		remove_action( 'wp_ajax_wds_analysis_remove_keyword', array( $this, 'remove_post_keyword' ) );
		remove_action( 'wp_ajax_wds_analysis_ignore_check', array( $this, 'json_set_ignore_check' ) );
		remove_action( 'wp_ajax_wds_analysis_unignore_check', array( $this, 'json_unset_ignore_check' ) );
		remove_action( 'admin_init', array( $this, 'set_up_post_columns' ) );
		remove_action( 'admin_enqueue_scripts', array( $this, 'inject_script_dependencies' ) );
		remove_action( 'post_submitbox_misc_actions', array( $this, 'add_postbox_fields' ) );

		return true;
	}

	/**
	 * Applies filtering by analysis threshold
	 *
	 * Used on post list table pages
	 *
	 * @param object $query Query to augment.
	 *
	 * @return void
	 */
	public function apply_analysis_post_list_filter( $query ) {
		$data = $_GET; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( ! isset( $data['wds_analysis_threshold'] ) && ! isset( $data['wds_readability_threshold'] ) ) {
			return;
		}

		if ( ! is_admin() ) {
			return;
		}

		if ( ! function_exists( 'get_current_screen' ) ) {
			return;
		}

		$screen = get_current_screen();

		if ( ! is_object( $screen ) || empty( $screen->base ) ) {
			return;
		}

		if ( 'edit' !== $screen->base ) {
			return;
		}

		$pt = ! empty( $screen->post_type ) ? $screen->post_type : false;

		if ( $query->get( 'post_type' ) !== $pt ) {
			return;
		}

		$meta_queries = $query->get( 'meta_query', array() );

		// Set SEO analysis threshold meta query.
		if ( isset( $data['wds_analysis_threshold'] ) ) {
			$raw = ! empty( $data['wds_analysis_threshold'] ) && is_numeric( $data['wds_analysis_threshold'] )
				? (int) $data['wds_analysis_threshold'] . ''
				: '69';

			if ( empty( $raw ) ) {
				return;
			}

			$rx = '';
			foreach ( str_split( $raw ) as $char ) {
				$rx .= ! empty( $char )
					? '[0-' . (int) $char . ']?'
					: '0?';
			}
			$rx             = substr( $rx, 0, strlen( $rx ) - 1 ); // Strip last question mark.
			$meta_queries[] = array(
				'key'     => Analysis_Model::META_KEY_ANALYSIS,
				'value'   => '[[:punct:]]percentage[[:punct:]];i:' . $rx . ';',
				'compare' => 'REGEXP',
			);
		}

		// Set readability meta query.
		if ( isset( $data['wds_readability_threshold'] ) ) {
			// Filter by just readable/not readable.
			$readable       = ! empty( $data['wds_readability_threshold'] ) ? 1 : 0;
			$meta_queries[] = array(
				'key'     => Analysis_Model::META_KEY_READABILITY,
				'value'   => '[[:punct:]]is_readable[[:punct:]];b:' . $readable . ';',
				'compare' => 'REGEXP',
			);
		}

		if ( ! empty( $meta_queries ) ) {
			$query->set( 'meta_query', $meta_queries );
		}
	}

	/**
	 * Enqueues admin scripts
	 *
	 * @param string $hook Page hook.
	 *
	 * @return void
	 */
	public function inject_script_dependencies( $hook ) {
		if ( 'edit.php' !== $hook ) {
			return;
		}

		wp_enqueue_script( Assets::WP_POST_LIST_TABLE_JS );
		wp_enqueue_style( Assets::WP_POST_LIST_TABLE_CSS );
	}

	/**
	 * Sets up column filtering actions
	 *
	 * @return void
	 */
	public function set_up_post_columns() {
		if ( ! Settings::get_setting( 'disable-analysis-on-list' ) ) {
			// Set up column filtering.
			foreach ( \smartcrawl_frontend_post_types() as $type ) {
				add_filter( "manage_{$type}_posts_columns", array( $this, 'add_analysis_columns' ) );
				add_action( "manage_{$type}_posts_custom_column", array( $this, 'add_analysis_column_data' ), 10, 2 );
			}
			// Add quick edit for focus keyword.
			add_action( 'quick_edit_custom_box', array( $this, 'add_quick_edit_focus_keyword_field' ) );
		}
	}

	/**
	 * Add Focus Keyword section.
	 *
	 * @param string $column Column name.
	 *
	 * @return void
	 */
	public function add_quick_edit_focus_keyword_field( $column ) {
		if ( 'seo' === $column ) {
			Simple_Renderer::render( 'post-list/quick-edit-seo-analysis' );
		}
	}

	/**
	 * Injects custom columns for analysis
	 *
	 * @param array $columns Columns hash.
	 *
	 * @return array
	 */
	public function add_analysis_columns( $columns ) {
		if ( Settings::get_setting( 'analysis-seo' ) ) {
			$columns['seo'] = __( 'SEO', 'wds' );
		}
		if ( Settings::get_setting( 'analysis-readability' ) ) {
			$columns['readability'] = __( 'Readability', 'wds' );
		}

		return $columns;
	}

	/**
	 * Adds custom columns analysis data
	 *
	 * @param string $cid     Column ID.
	 * @param int    $post_id Post ID.
	 *
	 * @return void
	 */
	public function add_analysis_column_data( $cid, $post_id ) {
		if ( ! in_array( $cid, array( 'seo', 'readability' ), true ) ) {
			return;
		}

		$result = $this->get_post_analysis_result_markup( $post_id );

		if ( 'seo' === $cid ) {
			echo wp_kses_post( $result['seo'] );
		}

		if ( 'readability' === $cid ) {
			echo wp_kses_post( $result['readability'] );
		}
	}

	/**
	 * Gets post analysis results markup for posts column
	 *
	 * @param int $post_id ID of the post.
	 *
	 * @return array List of column markups
	 */
	public function get_post_analysis_result_markup( $post_id ) {
		$model  = new Analysis_Model( $post_id );
		$result = array();
		$na     = '<div class="wds-analysis wds-status-invalid"><span>' . esc_html( __( 'N/A', 'wds' ) ) . '</span></div>';

		if ( ! $model->has_post_data( Analysis_Model::DATA_ANALYSIS ) ) {
			$result['seo'] = $na;
		} else {
			$data                     = $model->get_post_data( Analysis_Model::DATA_ANALYSIS );
			$focus_keywords           = \smartcrawl_get_value( 'focus-keywords', $post_id );
			$focus_keywords_available = ! empty( $focus_keywords );
			if ( ! $focus_keywords_available ) {
				$result['seo'] = Simple_Renderer::load(
					'post-list/post-seo-analysis-errors',
					array(
						'focus_missing' => true,
						'status_class'  => 'wds-status-invalid',
						'errors'        => array(
							'focus-keyword-missing' => esc_html__( 'You need to add focus keywords to see recommendations for this article.', 'wds' ),
						),
					)
				);
			} elseif ( empty( $data['errors'] ) ) {
				$result['seo'] = Simple_Renderer::load( 'post-list/post-seo-analysis-good' );
			} else {
				$result['seo'] = Simple_Renderer::load(
					'post-list/post-seo-analysis-errors',
					array(
						'errors' => $data['errors'],
					)
				);
			}
		}

		if ( ! $model->has_post_data( Analysis_Model::DATA_READABILITY ) ) {
			$result['readability'] = $na;
		} else {
			$data                  = $model->get_post_data( Analysis_Model::DATA_READABILITY );
			$readability_score     = intval( ceil( \smartcrawl_get_array_value( $data, 'score' ) ) );
			$readability_ignored   = Checks::is_readability_ignored( $post_id );
			$readability_state     = $model->get_kincaid_readability_state( $readability_score, $readability_ignored );
			$readability_class     = sprintf(
				'wds-status-%s',
				$readability_state
			);
			$readability_level     = $model->get_readability_level( false );
			$tag                   = \smartcrawl_get_array_value(
				$model->get_readability_levels_map(),
				array( $readability_level, 'tag' )
			);
			$tag                   = empty( $tag )
				? esc_html__( 'N/A', 'wds' )
				: $tag;
			$result['readability'] = '<div class="wds-analysis ' . $readability_class . '" title="' . $readability_score . '">' . esc_html( $tag ) . '</div>';
			if ( empty( $readability_score ) ) {
				$result['readability'] .= '<div class="wds-analysis-details">' . $readability_level . '</div>';
			}
		}

		return $result;
	}

	/**
	 * Handles check ignoring front-end requests
	 *
	 * @return void
	 */
	public function json_set_ignore_check() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		if ( empty( $data['check_id'] ) ) {
			wp_send_json_error();
		}

		Checks::add_ignored_check( (int) $data['post_id'], sanitize_text_field( $data['check_id'] ) );

		$model = new Analysis_Model( (int) $data['post_id'] );
		$model->clear_cached_data();

		wp_send_json_success();
	}

	/**
	 * Handles check de-ignoring front-end requests
	 *
	 * @return void
	 */
	public function json_unset_ignore_check() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		if ( empty( $data['check_id'] ) ) {
			wp_send_json_error();
		}

		Checks::remove_ignored_check( (int) $data['post_id'], sanitize_text_field( $data['check_id'] ) );

		$model = new Analysis_Model( (int) $data['post_id'] );
		$model->clear_cached_data();

		wp_send_json_success();
	}

	/**
	 * Add a new keyword to the post.
	 *
	 * @since 3.4.0
	 *
	 * @return void
	 */
	public function add_post_keyword() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) || empty( $data['keyword'] ) ) {
			wp_send_json_error();
		}

		// Get required data.
		$keyword = (string) \smartcrawl_get_array_value( $data, 'keyword' );
		$post_id = (int) \smartcrawl_get_array_value( $data, 'post_id' );

		// Permission check.
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error();
		}

		$smartcrawl_post = Post_Cache::get()->get_post( $post_id );

		if ( empty( $smartcrawl_post ) ) {
			wp_send_json_error();
		} else {
			$smartcrawl_post->add_focus_keyword( $keyword );
			$keywords = \smartcrawl_get_value( 'focus-keywords', $post_id );
			wp_send_json_success( empty( $keywords ) ? '' : $keywords );
		}
	}

	/**
	 * Remove an existing keyword from the post.
	 *
	 * @since 3.4.0
	 *
	 * @return void
	 */
	public function remove_post_keyword() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) || empty( $data['keyword'] ) ) {
			wp_send_json_error();
		}

		// Get required data.
		$keyword = (string) \smartcrawl_get_array_value( $data, 'keyword' );
		$post_id = (int) \smartcrawl_get_array_value( $data, 'post_id' );

		// Permission check.
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error();
		}

		$smartcrawl_post = Post_Cache::get()->get_post( $post_id );

		if ( empty( $smartcrawl_post ) ) {
			wp_send_json_error();
		} else {
			$smartcrawl_post->remove_focus_keyword( $keyword );
			$keywords = \smartcrawl_get_value( 'focus-keywords', $post_id );
			wp_send_json_success( empty( $keywords ) ? '' : $keywords );
		}
	}

	/**
	 * Sends postbox editor JSON response with detailed post analysis
	 *
	 * @return void
	 */
	public function retrieve_post_analysis() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		$post_id = (int) \smartcrawl_get_array_value( $data, 'post_id' );

		$keywords     = \smartcrawl_get_value( 'focus-keywords', $post_id );
		$new_keywords = \smartcrawl_get_array_value( $data, 'wds_focus_keywords' );

		if ( $keywords !== $new_keywords ) {
			$cached_post = Post_Cache::get()->get_post( $post_id );

			if ( empty( $cached_post ) ) {
				wp_send_json_error();
			} else {
				$cached_post->set_focus_keywords( explode( ',', $new_keywords ) );
			}
		}

		$is_dirty = (bool) \smartcrawl_get_array_value( $data, 'is_dirty' );
		$post     = get_post( $post_id );
		/**
		 * If there is_dirty flag is set i.e. are unsaved changes in the editor then we
		 * will fetch the latest post revision and analyze that.
		 */
		$post_to_analyze = $is_dirty
			? \smartcrawl_get_latest_post_version( $post_id )
			: $post;
		$this->analyze_post( $post_to_analyze->ID );

		$out = array();

		$seo_analysis = $this->retrieve_post_seo_analysis( $post );

		if ( ! empty( $seo_analysis ) ) {
			$out['seo'] = $seo_analysis;
		}

		$readability_analysis = $this->retrieve_post_readability_analysis( $post );

		if ( ! empty( $readability_analysis ) ) {
			$out['readability'] = $readability_analysis;
		}

		wp_send_json_success( $out );
	}

	/**
	 * Retrieve SEO analysis for post metabox.
	 *
	 * @param \WP_Post $post Post instance.
	 *
	 * @return array
	 */
	public function retrieve_post_seo_analysis( $post ) {
		if ( ! Settings::get_setting( 'analysis-seo' ) ) {
			return array();
		}

		// If no analysis data is available, run analysis first.
		$this->maybe_analyze_post( $post->ID );

		$model        = new Analysis_Model( $post->ID );
		$seo_data     = $model->get_post_data( Analysis_Model::DATA_ANALYSIS );
		$extra_checks = $model->get_post_data( Analysis_Model::DATA_ANALYSIS_EXTRA );

		$result = array();

		$post_cache      = Post_Cache::get()->get_post( $post->ID );
		$primary_keyword = $post_cache ? $post_cache->get_primary_keyword() : '';

		if ( $primary_keyword ) {
			$errors = \smartcrawl_get_array_value( $seo_data, 'errors' );

			$result['primary_keyword']     = $primary_keyword;
			$result['primary_checks']      = \smartcrawl_get_array_value( $seo_data, 'checks' );
			$result['primary_error_count'] = ! empty( $errors ) ? count( $errors ) : 0;
		}

		$extra_keywords = $post_cache ? $post_cache->get_extra_keywords() : array();

		if ( ! empty( $extra_keywords ) ) {
			$result['extra_keywords'] = $extra_keywords;
			$result['extra_checks']   = $extra_checks;
		}

		return $result;
	}

	/**
	 * Retrieve Readability analysis for post metabox.
	 *
	 * @param \WP_Post $post Post instance.
	 *
	 * @return array
	 */
	public function retrieve_post_readability_analysis( $post ) {
		if ( ! Settings::get_setting( 'analysis-readability' ) ) {
			return array();
		}

		// If no analysis data is available, run analysis first.
		$this->maybe_analyze_post( $post->ID );

		$model = new Analysis_Model( $post->ID );
		$data  = $model->get_post_data( Analysis_Model::DATA_READABILITY );
		$score = \smartcrawl_get_array_value( $data, 'score' );

		if ( null === $score ) {
			return array();
		}

		$score   = intval( ceil( $score ) );
		$level   = $model->get_readability_level();
		$ignored = Checks::is_readability_ignored( $post->ID );
		$state   = $model->get_kincaid_readability_state( $score, $ignored );

		$refresh_disabled = 'auto-draft' === get_post_status() ? 'disabled' : '';
		$whitelabel_class = White_Label::get()->summary_class();

		return array(
			'score'            => $score,
			'level'            => $level,
			'state'            => $state,
			'ignored'          => $ignored,
			'refresh_disabled' => $refresh_disabled,
			'whitelabel_class' => $whitelabel_class,
		);
	}

	/**
	 * Determine whether current post type requires analysis or not.
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return bool
	 */
	private function post_type_requires_analysis( $post_id ) {
		$post_type = get_post_type_object( get_post_type( $post_id ) );

		return 'revision' === $post_type->name || $post_type->public;
	}

	/**
	 * Update post analysis data
	 *
	 * Forcefully updates the post analysis data,
	 * no questions asked
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return bool Status
	 */
	public function analyze_post( $post_id ) {
		if (
			empty( $post_id )
			|| ! is_numeric( $post_id )
			|| ! $this->post_type_requires_analysis( $post_id )
		) {
			return false;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return false;
		}

		$model = new Analysis_Model( $post_id );
		$model->update_analysis_data();
		$model->update_readability_data();

		return true;
	}

	/**
	 * Update post analysis data only if there's no such data
	 *
	 * @param int $post_id Post ID.
	 *
	 * @return bool Whether we updated the analysis data or not
	 */
	public function maybe_analyze_post( $post_id ) {
		if ( empty( $post_id ) || ! is_numeric( $post_id ) ) {
			return false;
		}

		$analyzed = false;

		$model = new Analysis_Model( $post_id );

		if ( ! $model->has_post_data( Analysis_Model::DATA_ANALYSIS ) ) {
			if ( current_user_can( 'edit_post', $post_id ) ) {
				$model->update_analysis_data();
			}
			$analyzed = true;
		}

		if ( ! $model->has_post_data( Analysis_Model::DATA_READABILITY ) ) {
			if ( current_user_can( 'edit_post', $post_id ) ) {
				$model->update_readability_data();
			}
			$analyzed = true;
		}

		return $analyzed;
	}

	/**
	 * Injects postbox publish editor content
	 *
	 * @param \WP_Post $post Post instance.
	 *
	 * @return void
	 */
	public function add_postbox_fields( $post ) {
		$model                    = new Analysis_Model( $post->ID );
		$smartcrawl_post          = Post_Cache::get()->get_post( $post->ID );
		$focus_keywords           = $smartcrawl_post
			? $smartcrawl_post->get_focus_keywords()
			: array();
		$focus_keywords_available = ! empty( $focus_keywords );

		if ( in_array( get_post_status( $post ), array( 'draft', 'auto-draft' ), true ) ) {
			$result     = Checks::apply( $post->ID );
			$checks     = $result->get_applied_checks();
			$has_errors = false;
			foreach ( $checks as $chk ) {
				if ( empty( $chk['status'] ) && empty( $chk['ignored'] ) ) {
					$has_errors = true;
					break;
				}
			}
		} else {
			$seo_data   = $model->get_post_data( Analysis_Model::DATA_ANALYSIS );
			$has_errors = ! empty( $seo_data['errors'] );
		}

		if ( ! $focus_keywords_available ) {
			$seo_class = 'wds-status-invalid';
			$seo_text  = __( 'No Focus Keyword', 'wds' );
		} elseif ( $has_errors ) {
			$seo_class = 'wds-status-warning';
			$seo_text  = __( 'Needs Improvement', 'wds' );
		} else {
			$seo_class = 'wds-status-success';
			$seo_text  = __( 'Good', 'wds' );
		}

		$readability_data    = $model->get_post_data( Analysis_Model::DATA_READABILITY );
		$readability_score   = \smartcrawl_get_array_value( $readability_data, 'score', 0 );
		$readability_score   = intval( ceil( $readability_score ) );
		$readability_ignored = Checks::is_readability_ignored( $post->ID );
		$readability_state   = $model->get_kincaid_readability_state( $readability_score, $readability_ignored );
		$readability_class   = sprintf(
			'wds-status-%s',
			$readability_state
		);
		$readability_text    = $model->get_readability_level();

		?>
		<div class="wds-post-box-fields">
			<?php if ( Settings::get_setting( 'analysis-seo' ) ) : ?>
				<div class="misc-pub-section seo-analysis <?php echo esc_attr( $seo_class ); ?>">
					<i class="wds-icon-magnifying-glass-search"></i>
					<?php esc_html_e( 'SEO:', 'wds' ); ?> <b><?php echo esc_html( $seo_text ); ?></b>
				</div>
			<?php endif; ?>

			<?php if ( Settings::get_setting( 'analysis-readability' ) ) : ?>
				<div class="misc-pub-section readability-analysis <?php echo esc_attr( $readability_class ); ?>">
					<i class="wds-icon-monitor"></i>
					<?php esc_html_e( 'Readability:', 'wds' ); ?> <b><?php echo esc_html( $readability_text ); ?></b>
				</div>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Sends JSON response with post analysis data
	 *
	 * As a side effect, updates the analysis of needed
	 *
	 * @return void
	 */
	public function retrieve_post_analysis_data() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		$this->maybe_analyze_post( (int) $data['post_id'] );
		$model = new Analysis_Model( (int) $data['post_id'] );

		wp_send_json_success(
			array(
				'analysis'              => $model->get_post_data( Analysis_Model::DATA_ANALYSIS ),
				'readability'           => $model->get_post_data( Analysis_Model::DATA_READABILITY ),
				'readability_threshold' => $model->get_readability_threshold(),
				'readable'              => $model->is_readable(),
			)
		);
	}

	/**
	 * Sends JSON response with post analysis markup
	 *
	 * As a side effect, updates the analysis of needed
	 *
	 * @return void
	 */
	public function retrieve_post_analysis_markup() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		$this->maybe_analyze_post( (int) $data['post_id'] );
		$result = $this->get_post_analysis_result_markup( (int) $data['post_id'] );

		wp_send_json_success( $result );
	}

	/**
	 * Force analysis recheck and respond with column markup data
	 *
	 * @return void
	 */
	public function retrieve_post_analysis_recheck() {
		$data = $this->get_request_data();

		if ( empty( $data['post_id'] ) || ! is_numeric( $data['post_id'] ) ) {
			wp_send_json_error();
		}

		$this->analyze_post( (int) $data['post_id'] );
		$result = $this->get_post_analysis_result_markup( (int) $data['post_id'] );

		wp_send_json_success( $result );
	}

	/**
	 * Verify nonce for request.
	 *
	 * @return array|mixed
	 */
	private function get_request_data() {
		return isset( $_POST['_wds_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wds_nonce'] ) ), 'wds-metabox-nonce' ) ? stripslashes_deep( $_POST ) : array();
	}
}