| Current File : /home/abedqjib/krishanfoundation.com/wp-content/plugins/surerank/inc/api/post.php |
<?php
/**
* Post class
*
* Handles post related REST API endpoints for the SureRank plugin.
*
* @package SureRank\Inc\API
*/
namespace SureRank\Inc\API;
use SureRank\Inc\Functions\Defaults;
use SureRank\Inc\Functions\Get;
use SureRank\Inc\Functions\Helper;
use SureRank\Inc\Functions\Send_Json;
use SureRank\Inc\Functions\Settings;
use SureRank\Inc\Functions\Update;
use SureRank\Inc\Import_Export\Utils as ImportExportUtils;
use SureRank\Inc\Schema\SchemasApi;
use SureRank\Inc\Traits\Get_Instance;
use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Class Post
*
* Handles post related REST API endpoints.
*/
class Post extends Api_Base {
use Get_Instance;
/**
* Route Get Post Seo Data
*/
protected const POST_SEO_DATA = '/post/settings';
/**
* Route Get Post Content
*/
protected const POST_CONTENT = '/admin/post-content';
/**
* Route Get Posts List
*/
protected const POSTS_LIST = '/posts-list';
/**
* Constructor
*
* @since 1.0.0
*/
public function __construct() {
}
/**
* Register API routes.
*
* @since 1.0.0
* @return void
*/
public function register_routes() {
$namespace = $this->get_api_namespace();
$this->register_all_post_routes( $namespace );
}
/**
* Get post seo data
*
* @param WP_REST_Request<array<string, mixed>> $request Request object.
* @since 1.0.0
* @return void
*/
public function get_post_seo_data( $request ) {
$post_id = $request->get_param( 'post_id' );
$post_type = $request->get_param( 'post_type' );
$is_taxonomy = $request->get_param( 'is_taxonomy' );
$data = self::get_post_data_by_id( $post_id, $post_type, $is_taxonomy );
$decode_data = Utils::decode_html_entities_recursive( $data ) ?? $data;
Send_Json::success( $decode_data );
}
/**
* Get post data by id
*
* @param int $post_id Post id.
* @param string $post_type Post type.
* @param bool $is_taxonomy Is taxonomy.
* @return array<string, mixed>
*/
public static function get_post_data_by_id( $post_id, $post_type = 'post', $is_taxonomy = false ) {
$all_options = Settings::format_array( Defaults::get_instance()->get_post_defaults( false ) );
$global_values = Settings::get();
$extended_meta = Utils::get_extended_meta_values( $post_id, $post_type, $is_taxonomy );
// Merge extended meta templates into global defaults for preview fallback.
$global_with_emt = array_merge( $global_values, $extended_meta );
return [
'data' => array_intersect_key( Settings::prep_post_meta( $post_id, $post_type, $is_taxonomy ), $all_options ),
'global_default' => $global_with_emt,
];
}
/**
* Update seo data
*
* REST endpoint handler. Extracts params from the request, delegates to
* the transport-free save_post_seo_meta() helper, and emits the result
* as JSON. The helper is shared with the AJAX fallback registered in
* inc/ajax/save-endpoints.php so both paths produce identical side
* effects for identical input.
*
* @param WP_REST_Request<array<string, mixed>> $request Request object.
* @since 1.0.0
* @return void
*/
public function update_post_seo_data( $request ) {
$post_id = (int) $request->get_param( 'post_id' );
$data = (array) $request->get_param( 'metaData' );
$result = self::save_post_seo_meta( $post_id, $data );
if ( $result['success'] ) {
// REST reached and save succeeded — record as evidence for Site Health.
\SureRank\Inc\Functions\Rest_Observation::mark_reachable();
Send_Json::success( [ 'message' => $result['message'] ] );
}
Send_Json::error( [ 'message' => $result['message'] ] );
}
/**
* Save post SEO meta — transport-free core logic.
*
* Called by the REST endpoint handler above and by the AJAX fallback
* handler in inc/ajax/save-endpoints.php. Returns a result array rather
* than emitting a response, so both callers can transport it in their
* native format (wp_send_json / Send_Json).
*
* Side effects (on success):
* - Downloads external feature-image URLs and updates data in place
* - Writes post meta via update_post_meta_common()
* - Runs the surerank_run_post_seo_checks filter
* - Updates the global and per-post last-optimised timestamps
*
* Side effects (on failure): no timestamps are written. This matches
* the pre-refactor behaviour of update_post_seo_data() and avoids
* marking a post as "optimized" when the SEO checks errored.
*
* @param int $post_id Post ID to save meta against.
* @param array<string, mixed> $data Meta payload (already sanitised).
* @return array{success: bool, message: string}
* @since 1.x.x
*/
public static function save_post_seo_meta( int $post_id, array $data ): array {
self::update_feature_image_data( $post_id, $data );
self::update_post_meta_common( $post_id, $data );
$check_result = self::get_instance()->run_checks( $post_id );
if ( is_wp_error( $check_result ) ) {
return [
'success' => false,
'message' => __( 'Error while running SEO Checks.', 'surerank' ),
];
}
$current_time = time();
Update::option( 'surerank_last_optimized_on', $current_time ); // Site-wide last optimisation.
Update::post_meta( $post_id, 'surerank_post_optimized_at', $current_time ); // Per-post optimisation timestamp.
return [
'success' => true,
'message' => __( 'Data updated', 'surerank' ),
];
}
/**
* Update feature image data
*
* @param int $post_id Post ID.
* @param array<string, mixed> $data Data to update (passed by reference to update URLs and IDs).
* @return void
*/
public static function update_feature_image_data( int $post_id, array &$data ): void {
$image_fields = [
'facebook_image_url' => 'facebook_image_id',
'twitter_image_url' => 'twitter_image_id',
'featured_image_url' => 'featured_image_id',
];
foreach ( $image_fields as $url_field => $id_field ) {
if ( ! isset( $data[ $url_field ] ) || empty( $data[ $url_field ] ) ) {
continue;
}
$image_url = $data[ $url_field ];
if ( strpos( $image_url, home_url() ) !== false ) {
continue;
}
$download_result = self::download_external_image( $image_url, $post_id );
if ( $download_result['success'] ) {
$data[ $url_field ] = $download_result['data']['url'];
$data[ $id_field ] = $download_result['data']['attachment_id'];
if ( 'featured_image_url' === $url_field ) {
set_post_thumbnail( $post_id, $download_result['data']['attachment_id'] );
}
}
}
}
/**
* Download external image and save to WordPress media library
*
* Uses the centralized download_and_save_image() utility which includes
* SSRF protection and redirect blocking for security.
*
* @param string $image_url Image URL to download.
* @param int $post_id Post ID to associate the image with (unused, kept for compatibility).
* @return array<string, mixed> Result with success status and data.
*/
public static function download_external_image( string $image_url, int $post_id ): array {
$result = ImportExportUtils::download_and_save_image( $image_url );
if ( ! $result['success'] ) {
return $result;
}
$attachment_id = $result['data']['attachment_id'] ?? 0;
// Add generated image meta (in addition to _surerank_imported from utils).
if ( $attachment_id ) {
add_post_meta( $attachment_id, '_surerank_generated_image', true );
}
return $result;
}
/**
* Update post meta common
* This function updates the post meta for a given post ID with the provided data.
* It merges existing post meta with the new data, ensuring that all options are updated correctly.
*
* @param int $post_id Post ID.
* @param array<string, mixed> $data Data to update.
* @since 1.0.0
* @return void
*/
public static function update_post_meta_common( int $post_id, array $data ): void {
$all_options = Defaults::get_instance()->get_post_defaults( false );
/** Getting post meta if exists, otherwise getting all options(defaults) */
$post_meta = Get::all_post_meta( $post_id );
if ( ! empty( $post_meta ) ) {
$data = array_merge( $post_meta, $data );
}
$post_type = get_post_type( $post_id );
$post_type = is_string( $post_type ) ? $post_type : '';
$processed_options = Utils::process_option_values( $all_options, $data, $post_id, $post_type, false );
foreach ( $processed_options as $option_name => $new_option_value ) {
Update::post_meta( $post_id, 'surerank_settings_' . $option_name, $new_option_value );
}
}
/**
* Get post types
*
* @param WP_REST_Request<array<string, mixed>> $request Request object.
* @since 1.0.0
* @return void
*/
public function get_post_type_data( $request ) {
$data = $this->prepare_post_type_data();
Send_Json::success( [ 'data' => $data ] );
}
/**
* Run checks
*
* @param int $post_id Post ID.
* @return WP_Error|array<string, mixed>
*/
public function run_checks( $post_id ) {
if ( ! $post_id ) {
return new WP_Error( 'no_post_id', __( 'No post ID provided.', 'surerank' ) );
}
$post = get_post( $post_id );
if ( ! $post ) {
return new WP_Error( 'no_post', __( 'No post found.', 'surerank' ) );
}
return apply_filters( 'surerank_run_post_seo_checks', $post_id, $post );
}
/**
* Get posts list
*
* @param WP_REST_Request<array<string, mixed>> $request Request object.
* @since 1.6.3
* @return array<int, array<string, mixed>>
*/
public function get_posts_list( $request ) {
$search = $request->get_param( 'search' );
$page = $request->get_param( 'page' );
$per_page = $request->get_param( 'per_page' );
$post_type = $request->get_param( 'post_type' );
$exclude = $request->get_param( 'exclude' );
$args = [
'post_type' => $post_type,
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
's' => $search,
'fields' => 'ids',
];
if ( ! empty( $exclude ) ) {
$args['post__not_in'] = $exclude;
}
$schemas_api = SchemasApi::get_instance();
add_filter( 'posts_search', [ $schemas_api, 'search_only_titles' ], 10, 2 );
$query = new \WP_Query( $args );
remove_filter( 'posts_search', [ $schemas_api, 'search_only_titles' ], 10 );
$posts = [];
if ( $query->have_posts() ) {
foreach ( $query->posts as $post_id ) {
$posts[] = [
'label' => get_the_title( $post_id ),
'value' => $post_id,
];
}
}
return $posts;
}
/**
* Register all post routes
*
* @param string $namespace The API namespace.
* @return void
*/
private function register_all_post_routes( $namespace ) {
$this->register_get_post_seo_data_route( $namespace );
$this->register_update_post_seo_data_route( $namespace );
$this->register_post_content_route( $namespace );
$this->register_posts_list_route( $namespace );
}
/**
* Register get post SEO data route
*
* @param string $namespace The API namespace.
* @return void
*/
private function register_get_post_seo_data_route( $namespace ) {
register_rest_route(
$namespace,
self::POST_SEO_DATA,
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_post_seo_data' ],
'permission_callback' => [ $this, 'validate_permission' ],
'args' => $this->get_post_seo_data_args(),
'role_capability' => 'content_setting',
]
);
}
/**
* Register update post SEO data route
*
* @param string $namespace The API namespace.
* @return void
*/
private function register_update_post_seo_data_route( $namespace ) {
register_rest_route(
$namespace,
self::POST_SEO_DATA,
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'update_post_seo_data' ],
'permission_callback' => [ $this, 'validate_permission' ],
'args' => $this->get_update_post_seo_data_args(),
'role_capability' => 'content_setting',
]
);
}
/**
* Register post content route
*
* @param string $namespace The API namespace.
* @return void
*/
private function register_post_content_route( $namespace ) {
register_rest_route(
$namespace,
self::POST_CONTENT,
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_post_type_data' ],
'permission_callback' => [ $this, 'validate_permission' ],
'role_capability' => 'global_setting',
]
);
}
/**
* Register posts list route
*
* @since 1.6.3
* @param string $namespace The API namespace.
* @return void
*/
private function register_posts_list_route( $namespace ) {
register_rest_route(
$namespace,
self::POSTS_LIST,
[
'methods' => WP_REST_Server::READABLE,
'callback' => [ $this, 'get_posts_list' ],
'permission_callback' => [ $this, 'validate_permission' ],
'args' => $this->get_posts_list_args(),
]
);
}
/**
* Get posts list arguments
*
* @since 1.6.3
* @return array<string, array<string, mixed>>
*/
private function get_posts_list_args() {
return [
'search' => [
'type' => 'string',
'required' => false,
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'page' => [
'type' => 'integer',
'required' => false,
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'type' => 'integer',
'required' => false,
'default' => 20,
'sanitize_callback' => 'absint',
],
'post_type' => [
'type' => 'string',
'required' => false,
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
],
];
}
/**
* Get post SEO data arguments
*
* @return array<string, array<string, mixed>>
*/
private function get_post_seo_data_args() {
return [
'post_id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
],
'post_type' => [
'type' => 'string',
'required' => true,
'sanitize_callback' => 'sanitize_text_field',
],
];
}
/**
* Get update post SEO data arguments
*
* @return array<string, array<string, mixed>>
*/
private function get_update_post_seo_data_args() {
return [
'post_id' => [
'type' => 'integer',
'required' => true,
'sanitize_callback' => 'absint',
],
'metaData' => [
'type' => 'object',
'required' => true,
'sanitize_callback' => [ $this, 'sanitize_array_data' ],
],
];
}
/**
* Prepare post type data
*
* @return array<string, mixed>
*/
private function prepare_post_type_data() {
$data = [];
$settings = Settings::get();
$data['post_types'] = Helper::get_formatted_post_types();
$data['taxonomies'] = Helper::get_formatted_taxonomies();
$data['archives'] = $this->build_archives_list( $settings );
$data['roles'] = Helper::get_role_names();
return array_filter( $data );
}
/**
* Build archives list
*
* @param array<string, mixed> $settings Settings array.
* @return array<string, string>
*/
private function build_archives_list( $settings ) {
$archives = [];
if ( $settings['author_archive'] ?? false ) {
$archives['author'] = __( 'Author pages', 'surerank' );
}
if ( $settings['date_archive'] ?? false ) {
$archives['date'] = __( 'Date archives', 'surerank' );
}
$archives['search'] = __( 'Search pages', 'surerank' );
return $archives;
}
}