Your IP : 216.73.217.81


Current Path : /home3/vocl2671/liteccomputer/wp-content/plugins/mainwp-child/class/
Upload File :
Current File : //home3/vocl2671/liteccomputer/wp-content/plugins/mainwp-child/class/class-mainwp-child-posts.php

<?php
/**
 * MainWP Child posts handler
 *
 * This file handles all post & post plus actions.
 *
 * @package MainWP\Child
 */

namespace MainWP\Child;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

//phpcs:disable Generic.Metrics.CyclomaticComplexity -- Required to achieve desired results, pull request solutions appreciated.

/**
 * Class MainWP_Child_Posts
 *
 * Handle all post & post plus actions.
 */
class MainWP_Child_Posts { //phpcs:ignore -- NOSONAR - multi methods.

    /** Maximum durable receipts retained for each content protocol. */
    private const CONTENT_V2_MAX_RECORDS = 500;

    /** Minimum terminal-receipt retention in seconds. */
    private const CONTENT_V2_RETENTION = 7776000;

    /**
     * Public static variable to hold the single instance of MainWP_Child_Posts.
     *
     * @var mixed Default null
     */
    protected static $instance = null;

    /**
     * Comments and clauses.
     *
     * @var string Comments and clauses.
     */
    private $comments_and_clauses;

    /**
     * Posts with given suffix.
     *
     * @var string Posts with given suffix.
     */
    private $posts_where_suffix;

    /**
     * Get class name.
     *
     * @return string __CLASS__ Class name.
     */
    public static function get_class_name() {
        return __CLASS__;
    }

    /**
     * MainWP_Child_Posts constructor
     *
     * Run any time class is called.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::comments_and_clauses()
     * @uses \MainWP\Child\MainWP_Child_Posts::posts_where_suffix()
     */
    public function __construct() {
        $this->comments_and_clauses = '';
        $this->posts_where_suffix   = '';
    }

    /**
     * Create a public static instance of MainWP_Child_Posts.
     *
     * @return MainWP_Child_Posts|null
     */
    public static function get_instance() {
        if ( null === static::$instance ) {
            static::$instance = new self();
        }

        return static::$instance;
    }


    /**
     * Get recent posts.
     *
     * @param array  $pAllowedStatuses Array of allowed post statuses.
     * @param int    $pCount Number of posts.
     * @param string $type Post type.
     * @param null   $extra Extra tokens.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::get_recent_posts_int()
     *
     * @return array $allPost Return array of recent posts.
     */
    public function get_recent_posts( $pAllowedStatuses, $pCount, $type = 'post', $extra = null ) {
        $allPosts = array();
        if ( null !== $pAllowedStatuses ) {
            foreach ( $pAllowedStatuses as $status ) {
                $this->get_recent_posts_int( $status, $pCount, $type, $allPosts, $extra );
            }
        } else {
            $this->get_recent_posts_int( 'any', $pCount, $type, $allPosts, $extra );
        }

        return $allPosts;
    }

    /**
     * Initiate get recent posts.
     *
     * @param string $status Post status.
     * @param int    $pCount Number of posts.
     * @param string $type Post type.
     * @param array  $allPosts All posts array.
     * @param null   $extra Extra tokens.
     *
     * @uses \WPSEO_Link_Column_Count()
     * @uses \WPSEO_Meta()
     * @uses MainWP_WordPress_SEO::instance()::parse_column_score()
     * @uses MainWP_WordPress_SEO::instance()->parse_column_score_readability()
     * @uses \MainWP\Child\MainWP_Child_Posts::get_out_post()
     */
    public function get_recent_posts_int( $status, $pCount, $type, &$allPosts, $extra = null ) { //phpcs:ignore -- NOSONAR - complex.

        $args = array(
            'post_status'      => $status,
            'suppress_filters' => false,
            'post_type'        => $type,
        );

        $tokens = array();
        if ( is_array( $extra ) ) {
            if ( isset( $extra['tokens'] ) ) {
                $tokens = $extra['tokens'];
                if ( 1 === (int) $extra['extract_post_type'] ) {
                    $args['post_type'] = 'post';
                } elseif ( 2 === (int) $extra['extract_post_type'] ) {
                    $args['post_type'] = 'page';
                } elseif ( 3 === (int) $extra['extract_post_type'] ) {
                    $args['post_type'] = array( 'post', 'page' );
                }
            }
            if ( ! empty( $extra['ids'] ) && is_array( $extra['ids'] ) ) {
                $args['post__in']       = $extra['ids'];
                $args['posts_per_page'] = -1;
            }
        }
        $tokens = array_flip( $tokens );

        if ( 0 !== $pCount ) {
            $args['numberposts'] = $pCount;
        }
        // phpcs:disable WordPress.Security.NonceVerification
        $wp_seo_enabled = false;
        if ( ! empty( $_POST['WPSEOEnabled'] ) && is_plugin_active( 'wordpress-seo/wp-seo.php' ) && class_exists( '\WPSEO_Link_Column_Count' ) && class_exists( '\WPSEO_Meta' ) ) {
            $wp_seo_enabled = true;
        }
        // phpcs:enable WordPress.Security.NonceVerification
        $posts = get_posts( $args );

        if ( is_array( $posts ) ) {
            if ( $wp_seo_enabled ) {
                $post_ids = array();
                foreach ( $posts as $post ) {
                    $post_ids[] = $post->ID;
                }

                /**
                * Credits
                *
                * Plugin-Name: Yoast SEO
                * Plugin URI: https://yoast.com/wordpress/plugins/seo/#utm_source=wpadmin&utm_medium=plugin&utm_campaign=wpseoplugin
                * Author: Team Yoast
                * Author URI: https://yoast.com/
                * Licence: GPL v3
                *
                * The code is used for the MainWP WordPress SEO Extension
                * Extension URL: https://mainwp.com/extension/wordpress-seo/
                */
                $link_count = new \WPSEO_Link_Column_Count();
                $link_count->set( $post_ids );
            }
            foreach ( $posts as $post ) {
                $outPost = $this->get_out_post( $post, $extra, $tokens );
                if ( $wp_seo_enabled ) {
                    $outPost['seo_data'] = array(
                        'count_seo_links'   => $link_count->get( $post->ID, 'internal_link_count' ),
                        'count_seo_linked'  => $link_count->get( $post->ID, 'incoming_link_count' ),
                        'seo_score'         => MainWP_WordPress_SEO::instance()->parse_column_score( $post->ID ),
                        'readability_score' => MainWP_WordPress_SEO::instance()->parse_column_score_readability( $post->ID ),
                    );
                }
                $allPosts[] = $outPost;
            }
        }
    }

    /**
     * Build Post.
     *
     * @param array  $post Post array.
     * @param string $extra Post date & time.
     * @param array  $tokens Post tokens.
     * @return array $outPost Return completed post.
     */
    private function get_out_post( $post, $extra, $tokens ) { //phpcs:ignore -- NOSONAR - complex.
        $outPost                  = array();
        $outPost['id']            = $post->ID;
        $outPost['post_type']     = $post->post_type;
        $outPost['status']        = $post->post_status;
        $outPost['title']         = $post->post_title;
        $outPost['comment_count'] = $post->comment_count;
        if ( isset( $extra['where_post_date'] ) && ! empty( $extra['where_post_date'] ) ) {
            $outPost['dts'] = strtotime( $post->post_date_gmt );
        } else {
            $outPost['dts'] = strtotime( $post->post_modified_gmt );
        }

        if ( 'page' === $post->post_type ) {
            $outPost['dts'] = strtotime( $post->post_modified_gmt ); // to order by modified date.
        }

        if ( 'future' === $post->post_status ) {
            $outPost['dts'] = strtotime( $post->post_date_gmt );
        }

        $usr                    = get_user_by( 'id', $post->post_author );
        $outPost['author']      = ! empty( $usr ) ? $usr->user_nicename : 'removed';
        $outPost['authorEmail'] = ! empty( $usr ) ? $usr->user_email : 'removed';
        $categoryObjects        = get_the_category( $post->ID );
        $categories             = '';
        foreach ( $categoryObjects as $cat ) {
            if ( '' !== $categories ) {
                $categories .= ', ';
            }
            $categories .= $cat->name;
        }
        $outPost['categories'] = $categories;

        $tagObjects = get_the_tags( $post->ID );
        $tags       = '';
        if ( is_array( $tagObjects ) ) {
            foreach ( $tagObjects as $tag ) {
                if ( '' !== $tags ) {
                    $tags .= ', ';
                }
                $tags .= $tag->name;
            }
        }
        $outPost['tags'] = $tags;
        if ( is_array( $tokens ) ) {
            if ( isset( $tokens['[post.url]'] ) ) {
                $outPost['[post.url]'] = get_permalink( $post->ID );
            }
            if ( isset( $tokens['[post.website.url]'] ) ) {
                $outPost['[post.website.url]'] = get_site_url();
            }
            if ( isset( $tokens['[post.website.name]'] ) ) {
                $outPost['[post.website.name]'] = get_bloginfo( 'name' );
            }
        }
        return $outPost;
    }

    /**
     * Get all posts.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::get_all_posts_by_type()
     */
    public function get_all_posts() {
        // phpcs:disable WordPress.Security.NonceVerification
        $post_type = ( isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : 'post' );
        // phpcs:enable WordPress.Security.NonceVerification
        $this->get_all_posts_by_type( $post_type );
    }

    /**
     * Return one typed, bounded and cursor-bound post metadata page.
     *
     * @param mixed $request Decoded protocol request.
     * @return array Closed protocol response.
     */
    public function get_all_posts_v2( $request ) {
        $query = $this->normalize_posts_v2_request( $request );
        if ( false === $query ) {
            return $this->posts_v2_error( 'invalid_request' );
        }

        $generation = hash(
            'sha256',
            wp_json_encode(
                array_diff_key(
                    $query,
                    array(
                        'cursor' => true,
                        'offset' => true,
                    )
                )
            )
        );
        $offset     = 0;
        if ( null !== $query['cursor'] ) {
            $offset = $this->posts_v2_cursor_offset( $query['cursor'], $generation );
            if ( false === $offset ) {
                return $this->posts_v2_error( 'invalid_cursor' );
            }
        }

        $arguments = array(
            'post_type'           => $query['post_types'],
            'post_status'         => $query['statuses'],
            'posts_per_page'      => $query['page_size'] + 1,
            'offset'              => $offset,
            'orderby'             => 'ID',
            'order'               => 'ASC',
            'ignore_sticky_posts' => true,
            'no_found_rows'       => true,
        );
        if ( null !== $query['keyword'] ) {
            $arguments['s'] = $query['keyword'];
        }
        if ( null !== $query['post_id'] ) {
            $arguments['post__in'] = array( $query['post_id'] );
        }
        if ( null !== $query['author_id'] ) {
            $arguments['author'] = $query['author_id'];
        }
        if ( null !== $query['date_from'] || null !== $query['date_to'] ) {
            $date = array(
                'inclusive' => true,
                'column'    => 'post_date_gmt',
            );
            if ( null !== $query['date_from'] ) {
                $date['after'] = $query['date_from'] . ' 00:00:00';
            }
            if ( null !== $query['date_to'] ) {
                $date['before'] = $query['date_to'] . ' 23:59:59';
            }
            $arguments['date_query'] = array( $date );
        }

        $posts = get_posts( $arguments );
        if ( ! is_array( $posts ) ) {
            return $this->posts_v2_error( 'query_failed' );
        }
        $complete = count( $posts ) <= $query['page_size'];
        if ( ! $complete ) {
            array_pop( $posts );
        }

        $records = array();
        foreach ( $posts as $post ) {
            $record = $this->posts_v2_record( $post );
            if ( false === $record ) {
                return $this->posts_v2_error( 'record_invalid' );
            }
            $records[] = $record;
        }

        $next_offset = $offset + count( $records );
        return array(
            'protocol'         => '2',
            'operation'        => 'get_all_posts_v2',
            'ok'               => true,
            'complete'         => $complete,
            'records'          => $records,
            'next_cursor'      => $complete ? null : $this->posts_v2_cursor( $next_offset, $generation ),
            'query_generation' => $generation,
        );
    }

    /**
     * Negotiate the Post Dripper Child protocol.
     *
     * Publishing and receipt status use a durable idempotency contract.
     *
     * @param mixed $request Decoded protocol request.
     * @return array Closed protocol response.
     */
    public function post_dripper_capabilities_v2( $request ) {
        if ( ! is_array( $request ) || ! $this->posts_v2_exact_keys( $request, array( 'protocol', 'operation', 'payload' ) ) || '2' !== $request['protocol'] || ! is_string( $request['operation'] ) || 1 !== preg_match( '/^[a-z0-9_]{1,64}$/D', $request['operation'] ) || ! is_array( $request['payload'] ) ) {
            return $this->post_dripper_v2_error( 'unknown', 'invalid_request' );
        }

        $operation = $request['operation'];
        if ( 'capabilities' === $operation && array() === $request['payload'] ) {
            return array(
                'protocol'           => '2',
                'operation'          => 'capabilities',
                'ok'                 => true,
                'operations'         => array( 'post_dripper_delivery_v2', 'post_dripper_status_v2' ),
                'mutation_supported' => true,
            );
        }
        if ( 'post_dripper_delivery_v2' === $operation ) {
            return $this->content_v2_mutate( 'post_dripper', $operation, $request['payload'] );
        }
        if ( 'post_dripper_status_v2' === $operation ) {
            return $this->content_v2_status( 'post_dripper', $operation, $request['payload'], false );
        }

        return $this->post_dripper_v2_error( $operation, 'unsupported_operation' );
    }

    /**
     * Negotiate the Post Plus Child protocol.
     *
     * Post creation and receipt status use a durable idempotency contract.
     *
     * @param mixed $request Decoded protocol request.
     * @return array Closed protocol response.
     */
    public function post_plus_capabilities_v2( $request ) {
        if ( ! is_array( $request ) || ! $this->posts_v2_exact_keys( $request, array( 'protocol', 'operation', 'payload' ) ) || '2' !== $request['protocol'] || ! is_string( $request['operation'] ) || 1 !== preg_match( '/^[a-z0-9_]{1,64}$/D', $request['operation'] ) || ! is_array( $request['payload'] ) ) {
            return $this->post_plus_v2_error( 'unknown', 'invalid_request' );
        }

        $operation = $request['operation'];
        if ( 'capabilities' === $operation && array() === $request['payload'] ) {
            return array(
                'protocol'           => '2',
                'operation'          => 'capabilities',
                'ok'                 => true,
                'operations'         => array( 'post_plus_newpost_v2', 'post_plus_status_v2', 'post_plus_readback_v2' ),
                'mutation_supported' => true,
            );
        }
        if ( 'post_plus_newpost_v2' === $operation ) {
            return $this->content_v2_mutate( 'post_plus', $operation, $request['payload'] );
        }
        if ( 'post_plus_status_v2' === $operation ) {
            return $this->content_v2_status( 'post_plus', $operation, $request['payload'], false );
        }
        if ( 'post_plus_readback_v2' === $operation ) {
            if ( $this->posts_v2_exact_keys( $request['payload'], array( 'dashboard_ref', 'source_post_id', 'source_type' ) ) ) {
                return $this->post_plus_v2_source( $operation, $request['payload'] );
            }
            return $this->content_v2_status( 'post_plus', $operation, $request['payload'], true );
        }

        return $this->post_plus_v2_error( $operation, 'unsupported_operation' );
    }

    /**
     * Return one bounded immutable core post/page source without taking an edit lock.
     *
     * @param string $operation Closed operation name.
     * @param array  $payload   Exact source selector.
     * @return array Closed private source response.
     */
    private function post_plus_v2_source( $operation, $payload ) {
        $dashboard_ref = $this->content_v2_dashboard_ref();
        if ( false === $dashboard_ref
            || ! $this->posts_v2_exact_keys( $payload, array( 'dashboard_ref', 'source_post_id', 'source_type' ) )
            || ! $this->content_v2_hash( $payload['dashboard_ref'] )
            || ! hash_equals( $dashboard_ref, $payload['dashboard_ref'] )
            || ! is_int( $payload['source_post_id'] )
            || 1 > $payload['source_post_id']
            || ! in_array( $payload['source_type'], array( 'post', 'page' ), true ) ) {
            return $this->post_plus_v2_error( $operation, 'invalid_request' );
        }

        $source = get_post( $payload['source_post_id'] );
        if ( ! $source instanceof \WP_Post || $source->post_type !== $payload['source_type'] ) {
            return $this->post_plus_v2_error( $operation, 'source_not_found' );
        }
        $categories = 'post' === $source->post_type ? wp_get_post_terms( $source->ID, 'category', array( 'fields' => 'slugs' ) ) : array();
        $tags       = 'post' === $source->post_type ? wp_get_post_terms( $source->ID, 'post_tag', array( 'fields' => 'slugs' ) ) : array();
        if ( is_wp_error( $categories ) || is_wp_error( $tags ) || ! is_array( $categories ) || ! is_array( $tags ) || 100 < count( $categories ) || 100 < count( $tags ) ) {
            return $this->post_plus_v2_error( $operation, 'storage_unavailable' );
        }
        sort( $categories, SORT_STRING );
        sort( $tags, SORT_STRING );
        $post          = array(
            'post_type'      => $source->post_type,
            'status'         => in_array( $source->post_status, array( 'draft', 'publish' ), true ) ? $source->post_status : 'draft',
            'title'          => $source->post_title,
            'content'        => $source->post_content,
            'excerpt'        => $source->post_excerpt,
            'slug'           => $source->post_name,
            'comment_status' => $source->comment_status,
            'ping_status'    => $source->ping_status,
            'categories'     => array_values( $categories ),
            'tags'           => array_values( $tags ),
        );
        $randomization = array(
            'roles'           => array(),
            'random_category' => false,
            'date_from'       => null,
            'date_to'         => null,
            'timezone'        => 'UTC',
        );
        if ( false === $this->content_v2_normalize_post( $post ) || false === $this->content_v2_normalize_randomization( $randomization ) ) {
            return $this->post_plus_v2_error( $operation, 'unsupported_content' );
        }

        $compatibility = 'supported';
        $meta          = get_post_meta( $source->ID );
        if ( ! is_array( $meta ) ) {
            return $this->post_plus_v2_error( $operation, 'storage_unavailable' );
        }
        // One post can trip several blockers at once and only one of them is reported, so the
        // ranking is builder, then media, then meta - never whichever meta row happened to be
        // read last. Media outranks meta because an attached media object tells the operator to
        // deal with the attachment, while arbitrary meta tells them to decide what that meta
        // means; reporting the weaker one hides the actionable answer.
        foreach ( array_keys( $meta ) as $meta_key ) {
            if ( in_array( $meta_key, array( '_edit_last', '_edit_lock', '_encloseme', '_pingme', '_wp_page_template' ), true ) ) {
                continue;
            }
            if ( 1 === preg_match( '/(?:elementor|et_pb|fl_builder|beaver|brizy|oxygen)/i', $meta_key ) ) {
                $compatibility = 'unsupported_builder';
                break;
            }
            if ( '_thumbnail_id' === $meta_key ) {
                $compatibility = 'unsupported_media';
            } elseif ( 'supported' === $compatibility ) {
                $compatibility = 'unsupported_meta';
            }
        }
        if ( 'supported' === $compatibility && ( false !== stripos( $source->post_content, '<img' ) || false !== stripos( $source->post_content, '[gallery' ) ) ) {
            $compatibility = 'unsupported_media';
        }
        if ( 'supported' === $compatibility && '' !== $source->post_password ) {
            $compatibility = 'unsupported_meta';
        }

        $content_digest              = hash( 'sha256', wp_json_encode( array( $post, $randomization ) ) );
        $source_ref                  = hash_hmac( 'sha256', 'post-plus-source-v1|' . $source->ID . '|' . $source->post_type, wp_salt( 'auth' ) );
        $response                    = array(
            'protocol'        => '2',
            'operation'       => $operation,
            'complete'        => true,
            'source_post_id'  => (int) $source->ID,
            'source_type'     => $source->post_type,
            'source_ref'      => $source_ref,
            'title'           => $source->post_title,
            'content_digest'  => $content_digest,
            'compatibility'   => $compatibility,
            'private_context' => wp_json_encode(
                array(
                    'post'          => $post,
                    'randomization' => $randomization,
                )
            ),
        );
        $response['source_revision'] = hash( 'sha256', wp_json_encode( $response ) );
        return $response;
    }

    /**
     * Reserve and apply one typed post creation or update exactly once.
     *
     * @param string $protocol  Closed protocol name.
     * @param string $operation Closed operation name.
     * @param array  $payload   Untrusted decoded payload.
     * @return array Closed operation result.
     */
    private function content_v2_mutate( $protocol, $operation, $payload ) {
        $normalized = $this->content_v2_normalize_mutation( $protocol, $payload );
        if ( false === $normalized ) {
            return $this->content_v2_error( $protocol, $operation, 'invalid_request' );
        }

        $effect_hash = hash( 'sha256', wp_json_encode( $normalized ) );
        if ( ! $this->content_v2_begin_lock( $protocol ) ) {
            return $this->content_v2_error( $protocol, $operation, 'lock_busy' );
        }

        try {
            $repaired = false;
            $records  = $this->content_v2_records( $protocol, $repaired );
            if ( false === $records ) {
                return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
            }
            // A quarantine is stamped at first observation, so an unpersisted repair is re-stamped on
            // every request and can never grow old enough to be evicted or pruned - a ledger of damaged
            // entries would refuse every mutation forever. Persisting it here freezes those timestamps
            // and lets the entries age out normally.
            $pruned = $this->content_v2_prune_records( $protocol, $records, $repaired );
            if ( false !== $pruned ) {
                $records = $pruned;
            }

            $operation_ref = $normalized['operation_ref'];
            if ( isset( $records[ $operation_ref ] ) ) {
                if ( ! hash_equals( $records[ $operation_ref ]['dashboard_ref'], $normalized['dashboard_ref'] ) || ! hash_equals( $records[ $operation_ref ]['effect_hash'], $effect_hash ) ) {
                    return $this->content_v2_error( $protocol, $operation, 'request_conflict' );
                }
                // A settled receipt is answered from the ledger already read, writing nothing and
                // changing nothing, so a repair that could not be stored has no bearing on it -
                // refusing to state an outcome we are holding would be the worse failure.
                if ( 'reserved' !== $records[ $operation_ref ]['state'] && ! $records[ $operation_ref ]['retryable'] ) {
                    return $this->content_v2_project( $protocol, $operation, $records[ $operation_ref ] );
                }
            }
            // Everything past here writes, and reserving on top of a ledger whose repair was refused is
            // what puts the store back on the re-stamping loop above.
            if ( false === $pruned ) {
                return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
            }

            if ( isset( $records[ $operation_ref ] ) ) {
                if ( 'reserved' === $records[ $operation_ref ]['state'] ) {
                    $record = $this->content_v2_reconcile_reserved( $protocol, $records[ $operation_ref ], $records, 'reserved' );
                    if ( false === $record ) {
                        return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
                    }
                    if ( 'reserved' !== $record['state'] ) {
                        return $this->content_v2_project( $protocol, $operation, $record );
                    }
                } else {
                    $records[ $operation_ref ]['state']      = 'reserved';
                    $records[ $operation_ref ]['retryable']  = false;
                    $records[ $operation_ref ]['updated_at'] = time();
                    if ( ! $this->content_v2_write_records( $protocol, $records ) ) {
                        return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
                    }
                }
            } elseif ( $normalized['expires_at'] < time() - 60 ) {
                return $this->content_v2_error( $protocol, $operation, 'expired_request' );
            } elseif ( self::CONTENT_V2_MAX_RECORDS <= count( $records ) ) {
                $records = $this->content_v2_evict_records( $protocol, $records );
                if ( false === $records ) {
                    return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
                }
            }

            $target = null;
            if ( 'update' === $normalized['mode'] ) {
                $target = get_post( $normalized['target_post_id'] );
                if ( ! $target instanceof \WP_Post || $target->post_type !== $normalized['post']['post_type'] ) {
                    return $this->content_v2_error( $protocol, $operation, 'target_not_found' );
                }
                $current_revision = $this->content_v2_post_revision( $target->ID );
                if ( false === $current_revision || ! hash_equals( $normalized['expected_revision'], $current_revision ) ) {
                    return $this->content_v2_error( $protocol, $operation, 'stale_revision' );
                }
            }

            if ( ! isset( $records[ $operation_ref ] ) ) {
                $choices = 'post_plus' === $protocol ? $this->content_v2_random_choices( $normalized ) : array(
                    'author_id'     => $target instanceof \WP_Post ? (int) $target->post_author : get_current_user_id(),
                    'category_id'   => null,
                    'post_date_gmt' => null,
                );
                if ( false === $choices ) {
                    return $this->content_v2_error( $protocol, $operation, 'unsupported_content' );
                }
                if ( ! $this->content_v2_choices_available( $choices ) ) {
                    return $this->content_v2_error( $protocol, $operation, 'unsupported_content' );
                }
                $now                       = time();
                $records[ $operation_ref ] = array(
                    'dashboard_ref'     => $normalized['dashboard_ref'],
                    'operation_ref'     => $operation_ref,
                    'effect_hash'       => $effect_hash,
                    'content_digest'    => $normalized['content_digest'],
                    'mode'              => $normalized['mode'],
                    'target_post_id'    => $normalized['target_post_id'],
                    'expected_revision' => $normalized['expected_revision'],
                    'post_id'           => null,
                    'state'             => 'reserved',
                    'post_revision'     => null,
                    'remote_post_ref'   => null,
                    'retryable'         => false,
                    'choices'           => $choices,
                    'accepted_at'       => $now,
                    'updated_at'        => $now,
                );
                if ( ! $this->content_v2_write_records( $protocol, $records ) ) {
                    return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
                }
            } else {
                $choices = $records[ $operation_ref ]['choices'];
            }

            return $this->content_v2_apply( $protocol, $operation, $normalized, $records, $choices );
        } finally {
            $this->content_v2_end_lock( $protocol );
        }
    }

    /**
     * Return or repair one durable post-operation result.
     *
     * @param string $protocol  Closed protocol name.
     * @param string $operation Closed operation name.
     * @param array  $payload   Untrusted identity payload.
     * @param bool   $readback  Whether to add current redacted readback.
     * @return array Closed operation result.
     */
    private function content_v2_status( $protocol, $operation, $payload, $readback ) {
        $dashboard_ref = $this->content_v2_dashboard_ref();
        if ( ! is_array( $payload ) || ! $this->posts_v2_exact_keys( $payload, array( 'dashboard_ref', 'operation_ref' ) ) || ! $this->content_v2_hash( $payload['dashboard_ref'] ) || false === $dashboard_ref || ! hash_equals( $dashboard_ref, $payload['dashboard_ref'] ) || ! $this->content_v2_uuid( $payload['operation_ref'] ) ) {
            return $this->content_v2_error( $protocol, $operation, 'invalid_request' );
        }
        if ( ! $this->content_v2_begin_lock( $protocol ) ) {
            return $this->content_v2_error( $protocol, $operation, 'lock_busy' );
        }

        try {
            $records = $this->content_v2_records( $protocol );
            if ( false === $records ) {
                return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
            }
            $records = $this->content_v2_prune_records( $protocol, $records );
            if ( false === $records ) {
                return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
            }
            if ( ! isset( $records[ $payload['operation_ref'] ] ) || ! hash_equals( $records[ $payload['operation_ref'] ]['dashboard_ref'], $payload['dashboard_ref'] ) ) {
                return $this->content_v2_error( $protocol, $operation, 'operation_not_found' );
            }

            $record = $records[ $payload['operation_ref'] ];
            if ( 'reserved' === $record['state'] ) {
                $record = $this->content_v2_reconcile_reserved( $protocol, $record, $records, 'not_applied' );
                if ( false === $record ) {
                    return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
                }
            }

            return $this->content_v2_project( $protocol, $operation, $record, $readback );
        } finally {
            $this->content_v2_end_lock( $protocol );
        }
    }

    /**
     * Reconcile one durable reservation without repeating a proven effect.
     *
     * @param string $protocol   Closed protocol name.
     * @param array  $record     Valid reservation.
     * @param array  $records    Valid ledger, updated by reference.
     * @param string $zero_state State used when no committed post exists.
     * @return array|false Reconciled record or false on storage failure.
     */
    private function content_v2_reconcile_reserved( $protocol, $record, &$records, $zero_state ) {
        $matches = get_posts(
            array(
                'post_type'      => array( 'post', 'page' ),
                'post_status'    => array( 'publish', 'private', 'draft', 'pending', 'future', 'trash' ),
                'meta_key'       => '_mainwp_child_content_operation_v2', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bounded private reconciliation key.
                'meta_value'     => $record['operation_ref'], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Exact operation receipt binding.
                'posts_per_page' => 2,
            )
        );
        if ( ! is_array( $matches ) || 2 < count( $matches ) ) {
            return false;
        }
        $match_count = count( $matches );
        if ( 1 === $match_count ) {
            $revision = $this->content_v2_post_revision( $matches[0]->ID );
            if ( false === $revision ) {
                $record['state'] = 'unknown';
            } else {
                $record['post_id']         = $matches[0]->ID;
                $record['state']           = 'applied';
                $record['post_revision']   = $revision;
                $record['remote_post_ref'] = $this->content_v2_remote_ref( $protocol, $record['dashboard_ref'], $matches[0]->ID );
            }
        } elseif ( 0 === $match_count && 'not_applied' === $zero_state ) {
            $record['state']     = 'not_applied';
            $record['retryable'] = true;
        } elseif ( 0 === $match_count ) {
            return $record;
        } else {
            $record['state'] = 'unknown';
        }
        if ( 'unknown' === $record['state'] ) {
            $record['retryable'] = false;
        }
        $record['updated_at']                = time();
        $records[ $record['operation_ref'] ] = $record;
        return $this->content_v2_write_records( $protocol, $records ) ? $record : false;
    }

    /**
     * Normalize one closed mutation payload and verify its caller digest.
     *
     * @param string $protocol Closed protocol name.
     * @param mixed  $payload  Untrusted mutation payload.
     * @return array|false Normalized payload or false.
     */
    private function content_v2_normalize_mutation( $protocol, $payload ) {
        $keys = array( 'dashboard_ref', 'operation_ref', 'mode', 'target_post_id', 'expected_revision', 'content_digest', 'expires_at', 'post' );
        if ( 'post_plus' === $protocol ) {
            $keys[] = 'randomization';
        }
        $dashboard_ref = is_array( $payload ) && isset( $payload['dashboard_ref'] ) ? $this->content_v2_dashboard_ref() : false;
        if ( ! is_array( $payload ) || ! $this->posts_v2_exact_keys( $payload, $keys ) || ! $this->content_v2_hash( $payload['dashboard_ref'] ) || false === $dashboard_ref || ! hash_equals( $dashboard_ref, $payload['dashboard_ref'] ) || ! $this->content_v2_uuid( $payload['operation_ref'] ) || ! in_array( $payload['mode'], array( 'create', 'update' ), true ) || ! $this->content_v2_hash( $payload['content_digest'] ) || ! is_int( $payload['expires_at'] ) || time() + DAY_IN_SECONDS < $payload['expires_at'] ) {
            return false;
        }
        if ( ( 'create' === $payload['mode'] && ( null !== $payload['target_post_id'] || null !== $payload['expected_revision'] ) ) || ( 'update' === $payload['mode'] && ( ! is_int( $payload['target_post_id'] ) || 1 > $payload['target_post_id'] || ! $this->content_v2_hash( $payload['expected_revision'] ) ) ) ) {
            return false;
        }

        $post = $this->content_v2_normalize_post( $payload['post'] );
        if ( false === $post ) {
            return false;
        }
        $randomization = null;
        if ( 'post_plus' === $protocol ) {
            $randomization = $this->content_v2_normalize_randomization( $payload['randomization'] );
            if ( false === $randomization || ( 'page' === $post['post_type'] && $randomization['random_category'] ) ) {
                return false;
            }
        }
        $digest_value = 'post_plus' === $protocol ? array( $post, $randomization ) : $post;
        if ( ! hash_equals( $payload['content_digest'], hash( 'sha256', wp_json_encode( $digest_value ) ) ) ) {
            return false;
        }

        $normalized = array(
            'dashboard_ref'     => $payload['dashboard_ref'],
            'operation_ref'     => strtolower( $payload['operation_ref'] ),
            'mode'              => $payload['mode'],
            'target_post_id'    => $payload['target_post_id'],
            'expected_revision' => $payload['expected_revision'],
            'content_digest'    => $payload['content_digest'],
            'expires_at'        => $payload['expires_at'],
            'post'              => $post,
        );
        if ( 'post_plus' === $protocol ) {
            $normalized['randomization'] = $randomization;
        }
        return $normalized;
    }

    /**
     * Normalize one bounded core post payload.
     *
     * @param mixed $post Untrusted post payload.
     * @return array|false Normalized payload or false.
     */
    private function content_v2_normalize_post( $post ) {
        $keys = array( 'post_type', 'status', 'title', 'content', 'excerpt', 'slug', 'comment_status', 'ping_status', 'categories', 'tags' );
        if ( ! is_array( $post ) || ! $this->posts_v2_exact_keys( $post, $keys ) || ! in_array( $post['post_type'], array( 'post', 'page' ), true ) || ! in_array( $post['status'], array( 'draft', 'publish' ), true ) || ! in_array( $post['comment_status'], array( 'open', 'closed' ), true ) || ! in_array( $post['ping_status'], array( 'open', 'closed' ), true ) ) {
            return false;
        }
        foreach (
            array(
                'title'   => 512,
                'content' => 200000,
                'excerpt' => 5000,
                'slug'    => 200,
            ) as $key => $limit
        ) {
            if ( ! $this->content_v2_bounded_string( $post[ $key ], $limit ) ) {
                return false;
            }
        }
        if ( '' !== $post['slug'] && sanitize_title( $post['slug'] ) !== $post['slug'] ) {
            return false;
        }
        $categories = $this->content_v2_slug_list( $post['categories'] );
        $tags       = $this->content_v2_slug_list( $post['tags'] );
        if ( false === $categories || false === $tags ) {
            return false;
        }
        if ( 'page' === $post['post_type'] && ( ! empty( $categories ) || ! empty( $tags ) ) ) {
            return false;
        }
        $post['categories'] = $categories;
        $post['tags']       = $tags;
        return $post;
    }

    /**
     * Normalize Post Plus randomization without making a choice.
     *
     * @param mixed $randomization Untrusted randomization payload.
     * @return array|false Normalized randomization or false.
     */
    private function content_v2_normalize_randomization( $randomization ) {
        if ( ! is_array( $randomization ) || ! $this->posts_v2_exact_keys( $randomization, array( 'roles', 'random_category', 'date_from', 'date_to', 'timezone' ) ) || ! is_array( $randomization['roles'] ) || 4 < count( $randomization['roles'] ) || ! is_bool( $randomization['random_category'] ) || ! is_string( $randomization['timezone'] ) || '' === $randomization['timezone'] || 64 < strlen( $randomization['timezone'] ) ) {
            return false;
        }
        $roles = array_values( array_unique( $randomization['roles'] ) );
        sort( $roles, SORT_STRING );
        if ( count( $roles ) !== count( $randomization['roles'] ) || array_diff( $roles, array( 'administrator', 'editor', 'author', 'contributor' ) ) ) {
            return false;
        }
        try {
            $timezone = new \DateTimeZone( $randomization['timezone'] );
        } catch ( \Exception $error ) {
            unset( $error );
            return false;
        }
        $from = $this->content_v2_date( $randomization['date_from'], $timezone );
        $to   = $this->content_v2_date( $randomization['date_to'], $timezone );
        if ( false === $from || false === $to || ( null === $from ) !== ( null === $to ) || ( null !== $from && ( $to < $from || 366 < (int) $from->diff( $to )->format( '%a' ) ) ) ) {
            return false;
        }
        return array(
            'roles'           => $roles,
            'random_category' => $randomization['random_category'],
            'date_from'       => null === $from ? null : $from->format( 'Y-m-d' ),
            'date_to'         => null === $to ? null : $to->format( 'Y-m-d' ),
            'timezone'        => $timezone->getName(),
        );
    }

    /**
     * Resolve and freeze deterministic Post Plus author/category/date choices.
     *
     * @param array $normalized Valid normalized mutation.
     * @return array|false Frozen choices or false.
     */
    private function content_v2_random_choices( $normalized ) {
        $randomization = $normalized['randomization'];
        $seed          = hash( 'sha256', $normalized['dashboard_ref'] . '|' . $normalized['operation_ref'] . '|' . $normalized['content_digest'] );
        $author_ids    = array();
        if ( empty( $randomization['roles'] ) ) {
            $author_ids[] = get_current_user_id();
        } else {
            $users = get_users(
                array(
                    'role__in' => $randomization['roles'],
                    'fields'   => 'ids',
                    'number'   => 1001,
                )
            );
            if ( ! is_array( $users ) || 1000 < count( $users ) ) {
                return false;
            }
            foreach ( $users as $user_id ) {
                if ( is_int( $user_id ) && 1 <= $user_id ) {
                    $author_ids[] = $user_id;
                } elseif ( is_string( $user_id ) && 1 === preg_match( '/^[1-9][0-9]*$/D', $user_id ) && PHP_INT_MAX >= (float) $user_id ) {
                    $author_ids[] = (int) $user_id;
                }
            }
        }
        $author_ids = array_values( array_unique( $author_ids ) );
        sort( $author_ids, SORT_NUMERIC );
        if ( empty( $author_ids ) ) {
            return false;
        }

        $category_id = null;
        if ( $randomization['random_category'] ) {
            $categories = get_categories(
                array(
                    'hide_empty' => false,
                    'number'     => 1001,
                    'fields'     => 'ids',
                )
            );
            if ( ! is_array( $categories ) || empty( $categories ) || 1000 < count( $categories ) ) {
                return false;
            }
            $categories = array_values( array_unique( array_map( 'intval', $categories ) ) );
            if (
                array_filter(
                    $categories,
                    static function ( $category_id ) {
                        return 1 > $category_id;
                    }
                )
            ) {
                return false;
            }
            sort( $categories, SORT_NUMERIC );
            $category_id = $categories[ hexdec( substr( $seed, 8, 8 ) ) % count( $categories ) ];
        }

        $post_date_gmt = null;
        if ( null !== $randomization['date_from'] ) {
            $timezone      = new \DateTimeZone( $randomization['timezone'] );
            $from          = new \DateTimeImmutable( $randomization['date_from'] . ' 00:00:00', $timezone );
            $to            = new \DateTimeImmutable( $randomization['date_to'] . ' 23:59:59', $timezone );
            $seconds       = $to->getTimestamp() - $from->getTimestamp();
            $chosen        = $from->getTimestamp() + ( hexdec( substr( $seed, 16, 8 ) ) % ( $seconds + 1 ) );
            $post_date_gmt = gmdate( 'Y-m-d H:i:s', $chosen );
        }

        return array(
            'author_id'     => $author_ids[ hexdec( substr( $seed, 0, 8 ) ) % count( $author_ids ) ],
            'category_id'   => $category_id,
            'post_date_gmt' => $post_date_gmt,
        );
    }

    /**
     * Apply one reserved operation inside a checked database transaction.
     *
     * @param string $protocol   Closed protocol name.
     * @param string $operation  Closed operation name.
     * @param array  $normalized Valid normalized mutation.
     * @param array  $records    Valid durable ledger.
     * @param array  $choices    Frozen deterministic choices.
     * @return array Closed operation result.
     */
    private function content_v2_apply( $protocol, $operation, $normalized, $records, $choices ) {
        global $wpdb;

        $record = $records[ $normalized['operation_ref'] ];
        if ( ! $this->content_v2_choices_available( $choices ) ) {
            return $this->content_v2_error( $protocol, $operation, 'unsupported_content' );
        }
        $old_post   = 'update' === $normalized['mode'] ? get_post( $normalized['target_post_id'] ) : null;
        $old_status = $old_post instanceof \WP_Post ? $old_post->post_status : '';
        // Taken here because this is the last moment before the operation writes anything; the term
        // IDs content_v2_rollback() collects for its cache purge are read after the writes and cannot
        // stand in for it. The digest is snapshotted for the same reason and not compared against the
        // value this operation means to write: two Dashboards sending identical content produce the
        // same digest, so an equality check would convict every honest repeat of an unchanged update.
        $before = array(
            'post'   => $old_post,
            'terms'  => $old_post instanceof \WP_Post ? $this->content_v2_term_snapshot( $old_post->ID ) : null,
            'digest' => $old_post instanceof \WP_Post ? get_post_meta( $old_post->ID, '_mainwp_child_content_digest_v2', true ) : null,
        );
        if ( false === $wpdb->query( 'START TRANSACTION' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            return $this->content_v2_error( $protocol, $operation, 'storage_unavailable' );
        }

        $postarr = array(
            'post_type'      => $normalized['post']['post_type'],
            'post_status'    => $normalized['post']['status'],
            'post_title'     => $normalized['post']['title'],
            'post_content'   => $normalized['post']['content'],
            'post_excerpt'   => $normalized['post']['excerpt'],
            'post_name'      => $normalized['post']['slug'],
            'comment_status' => $normalized['post']['comment_status'],
            'ping_status'    => $normalized['post']['ping_status'],
            'post_author'    => $choices['author_id'],
            'meta_input'     => array(
                '_mainwp_child_content_operation_v2' => $normalized['operation_ref'],
                '_mainwp_child_content_digest_v2'    => $normalized['content_digest'],
            ),
        );
        if ( null !== $choices['post_date_gmt'] ) {
            $postarr['post_date_gmt'] = $choices['post_date_gmt'];
            $postarr['post_date']     = get_date_from_gmt( $choices['post_date_gmt'] );
        }
        if ( 'update' === $normalized['mode'] ) {
            $postarr['ID'] = $normalized['target_post_id'];
        }

        // Known hazard: a site listener that issues DDL trips MySQL's implicit commit, after which
        // content_v2_rollback() is a no-op. Firing the hook outside the transaction is not the fix -
        // the listener's own writes would then survive the rollback and run again on the Dashboard
        // retry, which is a common cost paid for a rare one. Instead the rollback reports whether it
        // actually landed, and the failures below stop claiming "no effect" when it did not.
        $hook_post = $postarr;
        unset( $hook_post['meta_input'] );
        do_action(
            'mainwp_before_post_update',
            $hook_post,
            array(),
            implode( ',', $normalized['post']['categories'] ),
            implode( ',', $normalized['post']['tags'] ),
            array()
        );

        $post_id = wp_insert_post( wp_slash( $postarr ), true );
        if ( is_wp_error( $post_id ) || ! is_int( $post_id ) || 1 > $post_id ) {
            $rolled_back = $this->content_v2_rollback( null, $normalized, $before );
            return $this->content_v2_error( $protocol, $operation, $this->content_v2_failure_code( $rolled_back ) );
        }
        if ( ! empty( $normalized['post']['categories'] ) ) {
            $category_ids = $this->content_v2_category_ids( $normalized['post']['categories'] );
            $terms        = false === $category_ids ? false : wp_set_post_categories( $post_id, $category_ids, false );
            if ( false === $terms || is_wp_error( $terms ) ) {
                $rolled_back = $this->content_v2_rollback( $post_id, $normalized, $before );
                return $this->content_v2_error( $protocol, $operation, $this->content_v2_failure_code( $rolled_back ) );
            }
        }
        if ( null !== $choices['category_id'] ) {
            $terms = wp_set_post_categories( $post_id, array( $choices['category_id'] ), true );
            if ( is_wp_error( $terms ) ) {
                $rolled_back = $this->content_v2_rollback( $post_id, $normalized, $before );
                return $this->content_v2_error( $protocol, $operation, $this->content_v2_failure_code( $rolled_back ) );
            }
        }
        if ( ! empty( $normalized['post']['tags'] ) ) {
            $terms = wp_set_post_terms( $post_id, $normalized['post']['tags'], 'post_tag', false );
            if ( is_wp_error( $terms ) ) {
                $rolled_back = $this->content_v2_rollback( $post_id, $normalized, $before );
                return $this->content_v2_error( $protocol, $operation, $this->content_v2_failure_code( $rolled_back ) );
            }
        }

        $revision = $this->content_v2_post_revision( $post_id );
        if ( false === $revision ) {
            $this->content_v2_rollback( $post_id, $normalized, $before );
            return $this->content_v2_error( $protocol, $operation, 'outcome_unknown' );
        }
        $record['post_id']                   = $post_id;
        $record['state']                     = 'applied';
        $record['post_revision']             = $revision;
        $record['remote_post_ref']           = $this->content_v2_remote_ref( $protocol, $record['dashboard_ref'], $post_id );
        $record['updated_at']                = time();
        $records[ $record['operation_ref'] ] = $record;
        if ( ! $this->content_v2_write_records( $protocol, $records ) || false === $wpdb->query( 'COMMIT' ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
            $this->content_v2_rollback( $post_id, $normalized, $before );
            wp_cache_delete( $this->content_v2_option( $protocol ), 'options' );
            return $this->content_v2_error( $protocol, $operation, 'outcome_unknown' );
        }

        $post = get_post( $post_id );
        do_action(
            'mainwp_child_after_newpost',
            array(
                'success'       => true,
                'link'          => get_permalink( $post_id ),
                'added_id'      => $post_id,
                'new_post_data' => $post instanceof \WP_Post ? array(
                    'post_id'       => $post_id,
                    'post_type'     => $post->post_type,
                    'post_title'    => $post->post_title,
                    'post_date'     => $post->post_date,
                    'post_date_gmt' => $post->post_date_gmt,
                    'new_status'    => $post->post_status,
                    'old_status'    => $old_status,
                    'singular_name' => strtolower( $this->get_post_type_name( $post->post_type ) ),
                    'is_editing'    => 'update' === $normalized['mode'] ? 1 : 0,
                ) : array(),
            )
        );
        return $this->content_v2_project( $protocol, $operation, $record );
    }

    /**
     * Roll the mutation transaction back and forget every post it warmed.
     *
     * The post writers prime the object cache before the transaction is decided, so a
     * persistent cache keeps serving the mutated row and meta after the database has
     * discarded them - while the Dashboard is told the mutation failed.
     *
     * The term writers do the same to the taxonomy side: wp_set_object_terms() moves term counts
     * and anything reading a term while the transaction is open caches the in-transaction row, so
     * the terms the mutation attached are collected before the ROLLBACK and forgotten after it.
     * Categories and tags are the whole taxonomy surface this writer touches.
     *
     * @param int|null $post_id    Post touched inside the transaction, when one exists.
     * @param array    $normalized Valid normalized mutation.
     * @param array    $before     Update target as it stood before the transaction opened: 'post'
     *                             (\WP_Post|null), 'terms' (array|null), 'digest' (string|null).
     * @return bool True when no trace of this operation survived, false when one did.
     */
    private function content_v2_rollback( $post_id, $normalized, $before ) {
        global $wpdb;

        $touched = array_values(
            array_unique(
                array_filter(
                    array( $post_id, 'update' === $normalized['mode'] ? $normalized['target_post_id'] : null ),
                    static function ( $id ) {
                        return is_int( $id ) && 0 < $id;
                    }
                )
            )
        );

        $term_ids = array();
        foreach ( array( 'category', 'post_tag' ) as $taxonomy ) {
            $ids = array() === $touched ? array() : wp_get_object_terms( $touched, $taxonomy, array( 'fields' => 'ids' ) );
            if ( is_array( $ids ) && array() !== $ids ) {
                $term_ids[ $taxonomy ] = $ids;
            }
        }

        $wpdb->query( 'ROLLBACK' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Transaction control has no wpdb wrapper, and caching a ROLLBACK is meaningless.

        foreach ( $touched as $id ) {
            clean_post_cache( $id );
        }
        foreach ( $term_ids as $taxonomy => $ids ) {
            clean_term_cache( $ids, $taxonomy );
        }

        // Whether the ROLLBACK landed is not knowable from its return value once a hook listener may
        // have closed the transaction under it, so the answer comes from storage instead. Only the
        // purge above makes that re-read honest - before it, the in-transaction row is still cached.
        // Every check below may only convict, never acquit: any one of them finding a trace is the
        // answer, and reaching the end without one is the only way to report a landed rollback. That
        // is why the operation stamp appears among them but never clears anything - wp_insert_post()
        // drops what update_post_meta() returns, so a post the implicit commit made durable can be
        // carrying no stamp at all.
        foreach ( $touched as $id ) {
            $post = get_post( $id );
            if ( ! $post instanceof \WP_Post ) {
                continue;
            }
            if ( 'update' !== $normalized['mode'] || (int) $id !== (int) $normalized['target_post_id'] ) {
                // Nothing but this operation's own insert could have created the row, so finding one
                // still there is the whole trace.
                return false;
            }
            if ( ! $before['post'] instanceof \WP_Post ) {
                // The witness is the pre-operation row, and without it there is nothing to compare
                // against. Guessing in the site's favour is the guess that sends the Dashboard back
                // to retry a write that may already be durable.
                return false;
            }
            if ( get_post_meta( $id, '_mainwp_child_content_operation_v2', true ) === $normalized['operation_ref'] ) {
                // Positive evidence only, which is not the check that was taken out of here. What was
                // wrong was reading a *missing* stamp as proof the rollback landed: wp_insert_post()
                // throws away what update_post_meta() returns, so a durable row can carry no stamp at
                // all. Finding this operation's own ref stored has no such hole - the stamp is written
                // inside the transaction and nothing outside this operation writes that value - so it
                // is the write itself outliving the ROLLBACK. An update target that was not restamped
                // still carries some earlier operation's ref, which never equals this one.
                return false;
            }
            // The stamp is not the only meta this operation writes, and meta_input writes each entry
            // on its own: the digest can land durably in the same breath the stamp write is refused,
            // which leaves the check above silent. It is compared against the snapshot rather than
            // against what this operation meant to store because a digest is not unique to an
            // operation the way a ref is - re-sending identical content produces the identical digest,
            // so an equality check would report outcome_unknown for every clean repeat. A snapshot
            // too broken to compare is a state that cannot be cleared, the same as a missing row.
            $stored_digest = get_post_meta( $id, '_mainwp_child_content_digest_v2', true );
            if ( ! is_string( $before['digest'] ) || $before['digest'] !== $stored_digest ) {
                return false;
            }
            // The question is whether the row is what it was before this operation, never whether
            // what the operation meant to write is there. Anything on wp_insert_post()'s own path -
            // wp_insert_post_data above all - may rewrite a field between the payload and storage,
            // so intended values can match nothing in the row while the write is perfectly durable;
            // the snapshot answers without reading the payload at all. The fields are the columns
            // wp_insert_post() writes, so no surviving part of this write can land outside them,
            // and post_modified_gmt is among them because the update path restamps it even when the
            // content it stored was unchanged.
            //
            // Equal columns end nothing on their own: an update that stored the values the row
            // already held leaves them all equal (down to a restamp inside the same second) while
            // the meta and term writes this operation also makes may be sitting there durable, so
            // the checks around this one are what answer for the rest of the write.
            $columns = array(
                'post_author',
                'post_date',
                'post_date_gmt',
                'post_content',
                'post_content_filtered',
                'post_title',
                'post_excerpt',
                'post_status',
                'post_type',
                'comment_status',
                'ping_status',
                'post_password',
                'post_name',
                'to_ping',
                'pinged',
                'post_modified',
                'post_modified_gmt',
                'post_parent',
                'menu_order',
                'post_mime_type',
                'guid',
            );
            foreach ( $columns as $field ) {
                if ( (string) $before['post']->$field !== (string) $post->$field ) {
                    return false;
                }
            }
            // The categories and tags are written after the row and are the rest of what a closed
            // transaction can leave behind, so the same question has to be put to them: is the
            // taxonomy what it was before this operation ran? Sets of IDs, sorted, because the order
            // wp_get_object_terms() returns is not a change. An unreadable state on either side is
            // a state that cannot be cleared, which is outcome_unknown for the same reason a missing
            // post snapshot is.
            $new_terms = $this->content_v2_term_snapshot( $id );
            if ( null === $before['terms'] || null === $new_terms || $before['terms'] !== $new_terms ) {
                return false;
            }
        }
        return true;
    }

    /**
     * Read one post's category and tag membership as a comparable set of term IDs.
     *
     * @param int $post_id Positive post ID.
     * @return array|null Sorted term IDs per taxonomy, or null when a taxonomy could not be read.
     */
    private function content_v2_term_snapshot( $post_id ) {
        $snapshot = array();
        foreach ( array( 'category', 'post_tag' ) as $taxonomy ) {
            $ids = wp_get_object_terms( $post_id, $taxonomy, array( 'fields' => 'ids' ) );
            if ( ! is_array( $ids ) ) {
                return null;
            }
            $ids = array_map( 'intval', $ids );
            sort( $ids, SORT_NUMERIC );
            $snapshot[ $taxonomy ] = $ids;
        }
        return $snapshot;
    }

    /**
     * Name the failure a rolled-back mutation may honestly claim.
     *
     * The Dashboard retries on mutation_failed, so that code may only be given when the site really
     * is untouched.
     *
     * @param bool $rolled_back Whether the rollback demonstrably left no trace.
     * @return string Closed error code.
     */
    private function content_v2_failure_code( $rolled_back ) {
        return $rolled_back ? 'mutation_failed' : 'outcome_unknown';
    }

    /**
     * Return one canonical revision for the actual stored post fields.
     *
     * @param int $post_id Positive post ID.
     * @return string|false Revision or false.
     */
    private function content_v2_post_revision( $post_id ) {
        $post = get_post( $post_id );
        if ( ! $post instanceof \WP_Post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
            return false;
        }
        $categories = wp_get_post_terms( $post->ID, 'category', array( 'fields' => 'slugs' ) );
        $tags       = wp_get_post_terms( $post->ID, 'post_tag', array( 'fields' => 'slugs' ) );
        if ( is_wp_error( $categories ) || is_wp_error( $tags ) || ! is_array( $categories ) || ! is_array( $tags ) ) {
            return false;
        }
        sort( $categories, SORT_STRING );
        sort( $tags, SORT_STRING );
        return hash(
            'sha256',
            wp_json_encode(
                array(
                    'post_type'      => $post->post_type,
                    'status'         => $post->post_status,
                    'title'          => $post->post_title,
                    'content'        => $post->post_content,
                    'excerpt'        => $post->post_excerpt,
                    'slug'           => $post->post_name,
                    'comment_status' => $post->comment_status,
                    'ping_status'    => $post->ping_status,
                    'author_id'      => (int) $post->post_author,
                    'post_date_gmt'  => $post->post_date_gmt,
                    'categories'     => $categories,
                    'tags'           => $tags,
                )
            )
        );
    }

    /**
     * Return one redacted operation response, optionally with current readback.
     *
     * @param string $protocol  Closed protocol name.
     * @param string $operation Closed operation name.
     * @param array  $record    Valid operation record.
     * @param bool   $readback  Whether to add current readback.
     * @return array Closed response.
     */
    private function content_v2_project( $protocol, $operation, $record, $readback = false ) {
        $response = array(
            'protocol'        => '2',
            'operation'       => $operation,
            'ok'              => true,
            'state'           => $record['state'],
            'operation_ref'   => $record['operation_ref'],
            'content_digest'  => $record['content_digest'],
            'post_revision'   => $record['post_revision'],
            'remote_post_ref' => $record['remote_post_ref'],
            'retryable'       => $record['retryable'],
            'updated_at'      => $record['updated_at'],
        );
        if ( $readback ) {
            $post     = is_int( $record['post_id'] ) ? get_post( $record['post_id'] ) : null;
            $revision = $post instanceof \WP_Post ? $this->content_v2_post_revision( $post->ID ) : false;
            if ( ! $post instanceof \WP_Post || false === $revision ) {
                return $this->content_v2_error( $protocol, $operation, 'readback_unavailable' );
            }
            $response['readback'] = array(
                'post_type' => $post->post_type,
                'status'    => $post->post_status,
                'revision'  => $revision,
            );
        }
        return $response;
    }

    /**
     * Load one bounded private operation ledger, keeping every entry that still means something.
     *
     * The option is untrusted input, and failing the whole ledger over one damaged entry would
     * cost every later mutation on this site. Dropping the entry is not free either: an entry
     * filed under a real operation reference is the evidence that this operation already ran
     * here, and without it the still-live original request reserves again and commits a second
     * post. So a damaged entry under a valid reference is quarantined rather than deleted, and
     * only an entry whose key is not an operation reference - which no request can ever replay
     * against - is dropped. Reserved receipts that still validate are untouched.
     *
     * @param string $protocol Closed protocol name.
     * @param bool   $repaired Receives whether the stored ledger differs from the returned one.
     * @return array|false Valid ledger or false.
     */
    private function content_v2_records( $protocol, &$repaired = null ) {
        $repaired = false;
        $records  = get_option( $this->content_v2_option( $protocol ), array() );
        if ( ! is_array( $records ) ) {
            return false;
        }
        $valid = array();
        foreach ( $records as $operation_ref => $record ) {
            if ( is_string( $operation_ref ) && $this->content_v2_record( $record ) && hash_equals( $operation_ref, $record['operation_ref'] ) ) {
                $valid[ $operation_ref ] = $record;
            } elseif ( $this->content_v2_uuid( $operation_ref ) ) {
                $valid[ $operation_ref ] = $this->content_v2_quarantine_record( $operation_ref, $record );
                $repaired                = true;
            } else {
                $repaired = true;
            }
        }
        return self::CONTENT_V2_MAX_RECORDS < count( $valid ) ? false : $valid;
    }

    /**
     * Rebuild one damaged entry as a quarantined unknown outcome under the same reference.
     *
     * The reference is what a retry is matched against, so keeping it is what stops the retry
     * from reserving a second time. What the damaged entry no longer proves is the outcome, so
     * everything it could have claimed about the effect is reset to the unknown, non-retryable
     * reading: no post id, no revision, no remote reference, and no route back into the apply.
     * The identity fields are kept where they still read, so a genuine retry is answered with
     * that unknown state instead of a conflict it cannot act on.
     *
     * @param string $operation_ref Valid operation reference taken from the ledger key.
     * @param mixed  $record        Damaged entry.
     * @return array Valid quarantined record.
     */
    private function content_v2_quarantine_record( $operation_ref, $record ) {
        $source      = is_array( $record ) ? $record : array();
        $unknown     = str_repeat( '0', 64 );
        $now         = time();
        $accepted_at = isset( $source['accepted_at'] ) && is_int( $source['accepted_at'] ) && 1 <= $source['accepted_at'] && $now >= $source['accepted_at'] ? $source['accepted_at'] : $now;
        $updated_at  = isset( $source['updated_at'] ) && is_int( $source['updated_at'] ) && $accepted_at <= $source['updated_at'] && $now >= $source['updated_at'] ? $source['updated_at'] : $now;
        return array(
            'dashboard_ref'     => isset( $source['dashboard_ref'] ) && $this->content_v2_hash( $source['dashboard_ref'] ) ? $source['dashboard_ref'] : $unknown,
            'operation_ref'     => $operation_ref,
            'effect_hash'       => isset( $source['effect_hash'] ) && $this->content_v2_hash( $source['effect_hash'] ) ? $source['effect_hash'] : $unknown,
            'content_digest'    => isset( $source['content_digest'] ) && $this->content_v2_hash( $source['content_digest'] ) ? $source['content_digest'] : $unknown,
            'mode'              => 'create',
            'target_post_id'    => null,
            'expected_revision' => null,
            'post_id'           => null,
            'state'             => 'unknown',
            'post_revision'     => null,
            'remote_post_ref'   => null,
            'retryable'         => false,
            'choices'           => array(
                'author_id'     => 1,
                'category_id'   => null,
                'post_date_gmt' => null,
            ),
            'accepted_at'       => $accepted_at,
            'updated_at'        => $updated_at,
        );
    }

    /**
     * Remove only receipts older than the required retry-retention window.
     *
     * @param string $protocol       Closed protocol name.
     * @param array  $records        Valid durable ledger.
     * @param bool   $persist_repair Whether the caller's load repaired the ledger and must store it.
     * @return array|false Pruned ledger or false.
     */
    private function content_v2_prune_records( $protocol, $records, $persist_repair = false ) {
        $minimum = time() - self::CONTENT_V2_RETENTION;
        $changed = $persist_repair;
        foreach ( $records as $operation_ref => $record ) {
            if ( $record['updated_at'] < $minimum ) {
                unset( $records[ $operation_ref ] );
                $changed = true;
            }
        }
        return ( ! $changed || $this->content_v2_write_records( $protocol, $records ) ) ? $records : false;
    }

    /**
     * Free one ledger slot by dropping only receipts that can no longer be replayed.
     *
     * The cap is reached long before the retention window expires, so pruning by age
     * alone leaves a full ledger rejecting every new mutation for the rest of the
     * ninety days. A request is accepted for at most a day (see the expires_at bound in
     * the payload validator), so a settled receipt older than that can only ever be
     * answered expired_request - dropping it cannot turn a replay into a second post.
     * Reserved receipts are never dropped: their effect is still unconfirmed, and losing
     * one is exactly what would let a retry duplicate content.
     *
     * @param string $protocol Closed protocol name.
     * @param array  $records  Valid durable ledger.
     * @return array|false Ledger below the cap, or false when no slot may be freed.
     */
    private function content_v2_evict_records( $protocol, $records ) {
        $horizon    = time() - ( DAY_IN_SECONDS + 60 );
        $candidates = array();
        foreach ( $records as $operation_ref => $record ) {
            if ( 'reserved' !== $record['state'] && $record['accepted_at'] < $horizon ) {
                $candidates[ $operation_ref ] = $record['accepted_at'];
            }
        }
        asort( $candidates, SORT_NUMERIC );
        foreach ( array_keys( $candidates ) as $operation_ref ) {
            if ( count( $records ) < self::CONTENT_V2_MAX_RECORDS ) {
                break;
            }
            unset( $records[ $operation_ref ] );
        }
        if ( self::CONTENT_V2_MAX_RECORDS <= count( $records ) ) {
            return false;
        }
        return $this->content_v2_write_records( $protocol, $records ) ? $records : false;
    }

    /**
     * Validate one closed durable content-operation record.
     *
     * @param mixed $record Candidate record.
     * @return bool Whether the record is valid.
     */
    private function content_v2_record( $record ) {
        $keys = array( 'dashboard_ref', 'operation_ref', 'effect_hash', 'content_digest', 'mode', 'target_post_id', 'expected_revision', 'post_id', 'state', 'post_revision', 'remote_post_ref', 'retryable', 'choices', 'accepted_at', 'updated_at' );
        if ( ! is_array( $record ) || array_keys( $record ) !== $keys || ! $this->content_v2_hash( $record['dashboard_ref'] ) || ! $this->content_v2_uuid( $record['operation_ref'] ) || ! $this->content_v2_hash( $record['effect_hash'] ) || ! $this->content_v2_hash( $record['content_digest'] ) || ! in_array( $record['mode'], array( 'create', 'update' ), true ) || ! in_array( $record['state'], array( 'reserved', 'applied', 'not_applied', 'unknown' ), true ) || ! is_bool( $record['retryable'] ) || ! is_int( $record['accepted_at'] ) || 1 > $record['accepted_at'] || ! is_int( $record['updated_at'] ) || $record['updated_at'] < $record['accepted_at'] ) {
            return false;
        }
        if ( ( 'create' === $record['mode'] && ( null !== $record['target_post_id'] || null !== $record['expected_revision'] ) ) || ( 'update' === $record['mode'] && ( ! is_int( $record['target_post_id'] ) || 1 > $record['target_post_id'] || ! $this->content_v2_hash( $record['expected_revision'] ) ) ) ) {
            return false;
        }
        if ( ! $this->content_v2_choice_shape( $record['choices'] ) ) {
            return false;
        }
        if ( 'applied' === $record['state'] ) {
            return is_int( $record['post_id'] ) && 1 <= $record['post_id'] && $this->content_v2_hash( $record['post_revision'] ) && $this->content_v2_hash( $record['remote_post_ref'] ) && false === $record['retryable'];
        }
        return null === $record['post_id'] && null === $record['post_revision'] && null === $record['remote_post_ref'] && ( ( 'not_applied' === $record['state'] ) === $record['retryable'] );
    }

    /**
     * Validate one persisted deterministic-choice tuple.
     *
     * @param mixed $choices Candidate choices.
     * @return bool Whether the choices are valid.
     */
    private function content_v2_choice_shape( $choices ) {
        return is_array( $choices ) && array_keys( $choices ) === array( 'author_id', 'category_id', 'post_date_gmt' ) && is_int( $choices['author_id'] ) && 1 <= $choices['author_id'] && ( null === $choices['category_id'] || ( is_int( $choices['category_id'] ) && 1 <= $choices['category_id'] ) ) && ( null === $choices['post_date_gmt'] || ( is_string( $choices['post_date_gmt'] ) && 1 === preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/D', $choices['post_date_gmt'] ) ) );
    }

    /**
     * Prove that frozen author/category choices still resolve before mutation.
     *
     * @param mixed $choices Candidate choices.
     * @return bool Whether all choices resolve.
     */
    private function content_v2_choices_available( $choices ) {
        if ( ! $this->content_v2_choice_shape( $choices ) || ! get_userdata( $choices['author_id'] ) instanceof \WP_User ) {
            return false;
        }
        if ( null === $choices['category_id'] ) {
            return true;
        }
        $category = get_term( $choices['category_id'], 'category' );
        return $category instanceof \WP_Term;
    }

    /**
     * Persist one changed operation ledger with nonautoloaded checked readback.
     *
     * @param string $protocol Closed protocol name.
     * @param array  $records  Valid changed ledger.
     * @return bool Whether exact persistence was proven.
     */
    private function content_v2_write_records( $protocol, $records ) {
        $option = $this->content_v2_option( $protocol );
        $wrote  = update_option( $option, $records, false );
        $stored = get_option( $option, null );
        return ( $wrote || $stored === $records ) && $stored === $records;
    }

    /**
     * Acquire the protocol's cross-request mutation lock.
     *
     * @param string $protocol Closed protocol name.
     * @return bool Whether the lock is held.
     */
    private function content_v2_begin_lock( $protocol ) {
        global $wpdb;
        $wpdb->last_error = '';
        $locked           = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,0)', 'mainwp_child_' . $protocol . '_v2' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        return empty( $wpdb->last_error ) && '1' === (string) $locked;
    }

    /**
     * Release the protocol lock and forget any unproved ownership.
     *
     * @param string $protocol Closed protocol name.
     * @return bool Whether release was proven.
     */
    private function content_v2_end_lock( $protocol ) {
        global $wpdb;
        $wpdb->last_error = '';
        $released         = $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', 'mainwp_child_' . $protocol . '_v2' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        return empty( $wpdb->last_error ) && '1' === (string) $released;
    }

    /**
     * Return the closed option name for one protocol.
     *
     * @param string $protocol Closed protocol name.
     * @return string Private option name.
     */
    private function content_v2_option( $protocol ) {
        return 'post_plus' === $protocol ? 'mainwp_child_post_plus_operations_v2' : 'mainwp_child_post_dripper_operations_v2';
    }

    /** Return the current connected Dashboard's canonical local identity digest. */
    private function content_v2_dashboard_ref() {
        $server = MainWP_Child_Keys_Manager::get_encrypted_option( 'mainwp_child_server', '' );
        if ( ! is_string( $server ) || '' === $server || 2048 < strlen( $server ) ) {
            return false;
        }
        $parts = wp_parse_url( $server );
        if ( ! is_array( $parts ) || ! isset( $parts['scheme'], $parts['host'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['query'] ) || isset( $parts['fragment'] ) ) {
            return false;
        }
        $port = isset( $parts['port'] ) && is_int( $parts['port'] ) && 1 <= $parts['port'] && 65535 >= $parts['port'] ? ':' . $parts['port'] : '';
        $path = isset( $parts['path'] ) && is_string( $parts['path'] ) ? '/' . trim( $parts['path'], '/' ) : '';
        if ( '/wp-admin' === substr( $path, -9 ) ) {
            $path = substr( $path, 0, -9 );
        }
        return hash( 'sha256', strtolower( $parts['scheme'] ) . '://' . strtolower( $parts['host'] ) . $port . rtrim( $path, '/' ) );
    }

    /**
     * Return one installation-bound opaque remote post reference.
     *
     * @param string $protocol      Closed protocol name.
     * @param string $dashboard_ref Current Dashboard digest.
     * @param int    $post_id       Positive post ID.
     * @return string Opaque post reference.
     */
    private function content_v2_remote_ref( $protocol, $dashboard_ref, $post_id ) {
        return hash_hmac( 'sha256', $protocol . '|' . $dashboard_ref . '|' . $post_id, wp_salt( 'auth' ) );
    }

    /**
     * Parse one exact calendar date without accepting PHP normalization.
     *
     * @param mixed         $value    Candidate date.
     * @param \DateTimeZone $timezone Valid timezone.
     * @return \DateTimeImmutable|null|false Exact date, null, or false.
     */
    private function content_v2_date( $value, $timezone ) {
        if ( null === $value ) {
            return null;
        }
        if ( ! is_string( $value ) || 1 !== preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/D', $value ) ) {
            return false;
        }
        $date   = \DateTimeImmutable::createFromFormat( '!Y-m-d', $value, $timezone );
        $errors = \DateTimeImmutable::getLastErrors();
        return false === $date || ( is_array( $errors ) && ( 0 !== $errors['warning_count'] || 0 !== $errors['error_count'] ) ) || $date->format( 'Y-m-d' ) !== $value ? false : $date;
    }

    /**
     * Normalize one unique bounded taxonomy-slug list.
     *
     * @param mixed $values Candidate list.
     * @return array|false Normalized list or false.
     */
    private function content_v2_slug_list( $values ) {
        if ( ! is_array( $values ) || 100 < count( $values ) ) {
            return false;
        }
        $out = array();
        foreach ( $values as $value ) {
            if ( ! is_string( $value ) || '' === $value || 200 < strlen( $value ) || sanitize_title( $value ) !== $value || isset( $out[ $value ] ) ) {
                return false;
            }
            $out[ $value ] = true;
        }
        $values = array_keys( $out );
        sort( $values, SORT_STRING );
        return $values;
    }

    /**
     * Resolve or create bounded category slugs as positive term IDs.
     *
     * @param array $slugs Valid canonical category slugs.
     * @return array|false Positive IDs or false.
     */
    private function content_v2_category_ids( $slugs ) {
        $ids = array();
        foreach ( $slugs as $slug ) {
            $term = get_term_by( 'slug', $slug, 'category' );
            if ( false === $term ) {
                $created = wp_insert_term( $slug, 'category', array( 'slug' => $slug ) );
                if ( is_wp_error( $created ) ) {
                    if ( 'term_exists' !== $created->get_error_code() || ! is_int( $created->get_error_data() ) ) {
                        return false;
                    }
                    $term_id = $created->get_error_data();
                } elseif ( ! is_array( $created ) || ! isset( $created['term_id'] ) || ! is_int( $created['term_id'] ) ) {
                    return false;
                } else {
                    $term_id = $created['term_id'];
                }
            } else {
                $term_id = $term instanceof \WP_Term ? (int) $term->term_id : 0;
            }
            if ( 1 > $term_id ) {
                return false;
            }
            $ids[] = $term_id;
        }
        return $ids;
    }

    /**
     * Validate one bounded UTF-8 string with no NUL byte.
     *
     * @param mixed $value Candidate string.
     * @param int   $max   Maximum bytes.
     * @return bool Whether the string is valid.
     */
    private function content_v2_bounded_string( $value, $max ) {
        return is_string( $value ) && $max >= strlen( $value ) && false === strpos( $value, "\0" ) && wp_check_invalid_utf8( $value ) === $value;
    }

    /**
     * Validate one SHA-256 hexadecimal value.
     *
     * @param mixed $value Candidate digest.
     * @return bool Whether the digest is valid.
     */
    private function content_v2_hash( $value ) {
        return is_string( $value ) && 1 === preg_match( '/^[a-f0-9]{64}$/D', $value );
    }

    /**
     * Validate one canonical RFC 4122 UUID.
     *
     * @param mixed $value Candidate reference.
     * @return bool Whether the reference is valid.
     */
    private function content_v2_uuid( $value ) {
        return is_string( $value ) && 1 === preg_match( '/^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/D', $value );
    }

    /**
     * Build one closed content-protocol error.
     *
     * @param string $protocol  Closed protocol name.
     * @param string $operation Closed operation name.
     * @param string $code      Stable error code.
     * @return array Closed error.
     */
    private function content_v2_error( $protocol, $operation, $code ) {
        return 'post_plus' === $protocol ? $this->post_plus_v2_error( $operation, $code ) : $this->post_dripper_v2_error( $operation, $code );
    }

    /**
     * Normalize one closed v2 query.
     *
     * @param mixed $request Decoded request.
     * @return array|false Normalized query or false.
     */
    private function normalize_posts_v2_request( $request ) {
        if ( ! is_array( $request ) || ! $this->posts_v2_exact_keys( $request, array( 'protocol', 'operation', 'query' ) ) || '2' !== $request['protocol'] || 'get_all_posts_v2' !== $request['operation'] || ! is_array( $request['query'] ) || ! $this->posts_v2_exact_keys( $request['query'], array( 'post_types', 'statuses', 'keyword', 'date_from', 'date_to', 'post_id', 'author_id', 'page_size', 'cursor' ) ) ) {
            return false;
        }

        $query = $request['query'];
        if ( ! $this->posts_v2_enum_list( $query['post_types'], array( 'post', 'page' ), 2 ) || ! $this->posts_v2_enum_list( $query['statuses'], array( 'publish', 'private', 'draft', 'pending', 'future', 'trash' ), 6 ) || ! is_int( $query['page_size'] ) || 1 > $query['page_size'] || 100 < $query['page_size'] || ( null !== $query['cursor'] && ( ! is_string( $query['cursor'] ) || 80 < strlen( $query['cursor'] ) ) ) ) {
            return false;
        }
        if ( null !== $query['keyword'] && ( ! is_string( $query['keyword'] ) || 200 < $this->posts_v2_length( $query['keyword'] ) || wp_check_invalid_utf8( $query['keyword'] ) !== $query['keyword'] ) ) {
            return false;
        }
        if ( ! $this->posts_v2_nullable_positive_integer( $query['post_id'] ) || ! $this->posts_v2_nullable_positive_integer( $query['author_id'] ) || ( null !== $query['post_id'] && null !== $query['author_id'] ) ) {
            return false;
        }
        if ( ! $this->posts_v2_date( $query['date_from'] ) || ! $this->posts_v2_date( $query['date_to'] ) ) {
            return false;
        }
        if ( null !== $query['date_from'] && null !== $query['date_to'] ) {
            $from = strtotime( $query['date_from'] . ' 00:00:00 UTC' );
            $to   = strtotime( $query['date_to'] . ' 23:59:59 UTC' );
            if ( false === $from || false === $to || $from > $to || 366 * DAY_IN_SECONDS < $to - $from ) {
                return false;
            }
        }

        sort( $query['post_types'] );
        sort( $query['statuses'] );
        return $query;
    }

    /**
     * Project one bounded record.
     *
     * @param \WP_Post $post Post row.
     * @return array|false Record or false.
     */
    private function posts_v2_record( $post ) {
        if ( ! $post instanceof \WP_Post || 1 > (int) $post->ID || ! in_array( $post->post_status, array( 'publish', 'private', 'draft', 'pending', 'future', 'trash' ), true ) ) {
            return false;
        }
        $url    = $this->posts_v2_url( get_permalink( $post ) );
        $title  = $this->posts_v2_string( get_the_title( $post ), 500 );
        $author = $this->posts_v2_string( get_the_author_meta( 'display_name', $post->post_author ), 200 );
        $date   = get_post_time( 'Y-m-d\TH:i:s\Z', true, $post );
        if ( false === $url || false === $title || false === $author || ! is_string( $date ) ) {
            return false;
        }
        return array(
            'post_id' => (int) $post->ID,
            'title'   => $title,
            'url'     => $url,
            'date'    => $date,
            'status'  => $post->post_status,
            'author'  => $author,
        );
    }

    /**
     * Remove userinfo and common secret query fields from a post URL.
     *
     * @param mixed $url Candidate URL.
     * @return string|false Safe URL or false.
     */
    private function posts_v2_url( $url ) {
        if ( ! is_string( $url ) || 2048 < strlen( $url ) ) {
            return false;
        }
        $parts = wp_parse_url( $url );
        if ( ! is_array( $parts ) || ! isset( $parts['scheme'], $parts['host'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
            return false;
        }
        return remove_query_arg( array( '_wpnonce', 'nonce', 'token', 'access_token', 'key', 'signature', 'password', 'auth', 'code' ), $url );
    }

    /**
     * Build an authenticated opaque page cursor.
     *
     * @param int    $offset Next result offset.
     * @param string $generation Query generation.
     * @return string Cursor.
     */
    private function posts_v2_cursor( $offset, $generation ) {
        $mac = hash_hmac( 'sha256', $generation . ':' . $offset, wp_salt( 'auth' ), true );
        return $offset . '.' . rtrim( strtr( base64_encode( $mac ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- base64url-encodes an HMAC for the cursor, not code.
    }

    /**
     * Verify one cursor and return its offset.
     *
     * @param string $cursor Cursor.
     * @param string $generation Query generation.
     * @return int|false Offset or false.
     */
    private function posts_v2_cursor_offset( $cursor, $generation ) {
        if ( 1 !== preg_match( '/^([1-9][0-9]{0,4})\.([A-Za-z0-9_-]{43})$/D', $cursor, $matches ) ) {
            return false;
        }
        $offset   = (int) $matches[1];
        $expected = $this->posts_v2_cursor( $offset, $generation );
        return 10000 >= $offset && hash_equals( $expected, $cursor ) ? $offset : false;
    }

    /**
     * Whether a list holds unique values drawn only from the allowed set.
     *
     * @param mixed $values  Values to check.
     * @param array $allowed Allowed values.
     * @param int   $maximum Maximum number of values.
     * @return bool
     */
    private function posts_v2_enum_list( $values, $allowed, $maximum ) {
        if ( ! is_array( $values ) || empty( $values ) || $maximum < count( $values ) || count( $values ) !== count( array_unique( $values, SORT_REGULAR ) ) ) {
            return false;
        }
        foreach ( $values as $value ) {
            if ( ! is_string( $value ) || ! in_array( $value, $allowed, true ) ) {
                return false;
            }
        }
        return true;
    }

    /**
     * Whether a value is null or a positive integer.
     *
     * @param mixed $value Value to check.
     * @return bool
     */
    private function posts_v2_nullable_positive_integer( $value ) {
        return null === $value || ( is_int( $value ) && 1 <= $value );
    }

    /**
     * Whether a value is null or a real calendar date in Y-m-d form.
     *
     * @param mixed $value Value to check.
     * @return bool
     */
    private function posts_v2_date( $value ) {
        if ( null === $value ) {
            return true;
        }
        if ( ! is_string( $value ) || 1 !== preg_match( '/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/D', $value ) ) {
            return false;
        }
        $parts = array_map( 'intval', explode( '-', $value ) );
        return checkdate( $parts[1], $parts[2], $parts[0] );
    }

    /**
     * Character length of a string, falling back to bytes without mbstring.
     *
     * @param string $value String to measure.
     * @return int
     */
    private function posts_v2_length( $value ) {
        return function_exists( 'mb_strlen' ) ? mb_strlen( $value, 'UTF-8' ) : strlen( $value );
    }

    /**
     * Accept a UTF-8 string no longer than the given character limit.
     *
     * @param mixed $value   Value to check.
     * @param int   $maximum Maximum length in characters.
     * @return string|false
     */
    private function posts_v2_string( $value, $maximum ) {
        return is_string( $value ) && wp_check_invalid_utf8( $value ) === $value && $maximum >= $this->posts_v2_length( $value ) ? $value : false;
    }

    /**
     * Whether an array holds exactly the given keys, no more and no fewer.
     *
     * @param mixed $value Value to inspect.
     * @param array $keys  Required keys.
     * @return bool
     */
    private function posts_v2_exact_keys( $value, $keys ) {
        if ( ! is_array( $value ) ) {
            return false;
        }
        $actual = array_keys( $value );
        sort( $actual );
        sort( $keys );
        return $actual === $keys;
    }

    /**
     * Build a closed get_all_posts_v2 protocol error.
     *
     * @param string $code Stable error code.
     * @return array
     */
    private function posts_v2_error( $code ) {
        return array(
            'protocol'  => '2',
            'operation' => 'get_all_posts_v2',
            'ok'        => false,
            'code'      => $code,
        );
    }

    /**
     * Build a closed Post Dripper protocol error.
     *
     * @param mixed  $operation Operation name.
     * @param string $code      Stable error code.
     * @return array
     */
    private function post_dripper_v2_error( $operation, $code ) {
        return array(
            'protocol'  => '2',
            'operation' => is_string( $operation ) && '' !== $operation ? $operation : 'unknown',
            'ok'        => false,
            'code'      => $code,
        );
    }

    /**
     * Build a closed Post Plus protocol error.
     *
     * @param mixed  $operation Operation name.
     * @param string $code      Stable error code.
     * @return array
     */
    private function post_plus_v2_error( $operation, $code ) {
        return array(
            'protocol'  => '2',
            'operation' => is_string( $operation ) && '' !== $operation ? $operation : 'unknown',
            'ok'        => false,
            'code'      => $code,
        );
    }

    /**
     * Get all pages.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::get_all_posts_by_type()
     */
    public function get_all_pages() {
        $this->get_all_posts_by_type( 'page' );
    }

    /**
     * Append the Post's SQL WHERE clause suffix.
     *
     * @param string $where Post's SQL WHERE clause.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::posts_where_suffix()
     *
     * @return string $where The full SQL WHERE clause with the appended suffix.
     */
    public function posts_where( $where ) {
        if ( $this->posts_where_suffix ) {
            $where .= ' ' . $this->posts_where_suffix;
        }

        return $where;
    }

    /**
     * Get all posts by type.
     *
     * @param string $type Post type.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::posts_where_suffix()
     * @uses \MainWP\Child\MainWP_Helper::write()
     */
    public function get_all_posts_by_type( $type ) { //phpcs:ignore -- NOSONAR - complex.

        /**
         * Object, providing access to the WordPress database.
         *
         * @global object $wpdb WordPress Database instance.
         */
        global $wpdb;

        // phpcs:disable WordPress.Security.NonceVerification
        add_filter( 'posts_where', array( &$this, 'posts_where' ) );
        $where_post_date = isset( $_POST['where_post_date'] ) && ! empty( $_POST['where_post_date'] ) ? true : false;
        if ( isset( $_POST['postId'] ) ) {
            $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.ID = %d ", sanitize_text_field( wp_unslash( $_POST['postId'] ) ) );
        } elseif ( isset( $_POST['userId'] ) ) {
            $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.post_author = %d ", sanitize_text_field( wp_unslash( $_POST['userId'] ) ) );
        } else {
            if ( isset( $_POST['keyword'] ) && '' !== $_POST['keyword'] ) {
                $search_on = isset( $_POST['search_on'] ) ? sanitize_text_field( wp_unslash( $_POST['search_on'] ) ) : '';
                if ( 'title' === $search_on ) {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND ( $wpdb->posts.post_title LIKE %s ) ", '%' . $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) ) . '%' );
                } elseif ( 'content' === $search_on ) {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND ( $wpdb->posts.post_content LIKE %s )", '%' . $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) ) . '%' );
                } else {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND ( $wpdb->posts.post_content LIKE %s OR $wpdb->posts.post_title LIKE  %s )", '%' . $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) ) . '%', '%' . $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['keyword'] ) ) ) . '%' );
                }
            }
            if ( isset( $_POST['dtsstart'] ) && '' !== $_POST['dtsstart'] ) {
                if ( $where_post_date ) {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.post_date > %s", $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['dtsstart'] ) ) ) );
                } else {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.post_modified > %s", $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['dtsstart'] ) ) ) );
                }
            }
            if ( isset( $_POST['dtsstop'] ) && '' !== $_POST['dtsstop'] ) {
                if ( $where_post_date ) {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.post_date < %s ", $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['dtsstop'] ) ) ) );
                } else {
                    $this->posts_where_suffix .= $wpdb->prepare( " AND $wpdb->posts.post_modified < %s", $wpdb->esc_like( sanitize_text_field( wp_unslash( $_POST['dtsstop'] ) ) ) );
                }
            }

            if ( isset( $_POST['exclude_page_type'] ) && sanitize_text_field( wp_unslash( $_POST['exclude_page_type'] ) ) ) {
                $this->posts_where_suffix .= " AND $wpdb->posts.post_type NOT IN ('page')";
            }
        }

        $maxPages = 50;
        if ( defined( 'MAINWP_CHILD_NR_OF_PAGES' ) ) {
            $maxPages = MAINWP_CHILD_NR_OF_PAGES;
        }

        if ( isset( $_POST['maxRecords'] ) ) {
            $maxPages = ! empty( $_POST['maxRecords'] ) ? intval( wp_unslash( $_POST['maxRecords'] ) ) : 0;
        }
        if ( 0 === $maxPages ) {
            $maxPages = 99999;
        }

        $extra = array();
        if ( isset( $_POST['extract_tokens'] ) ) {
            $extra['tokens']            = isset( $_POST['extract_tokens'] ) ? json_decode( base64_decode( wp_unslash( $_POST['extract_tokens'] ) ), true ) : ''; // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- base64_encode function is used for http encode compatible..
            $extra['extract_post_type'] = isset( $_POST['extract_post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['extract_post_type'] ) ) : '';
        }

        $extra['where_post_date'] = $where_post_date;
        $rslt                     = isset( $_POST['status'] ) ? $this->get_recent_posts( explode( ',', sanitize_text_field( wp_unslash( $_POST['status'] ) ) ), $maxPages, $type, $extra ) : '';
        $this->posts_where_suffix = '';
        // phpcs:enable WordPress.Security.NonceVerification

        MainWP_Helper::write( $rslt );
    }

    /**
     * Build New Post.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::create_post()
     * @uses \MainWP\Child\MainWP_Helper::instance()->error()
     * @uses \MainWP\Child\MainWP_Helper::write()
     */
    public function new_post() {
        // phpcs:disable WordPress.Security.NonceVerification
        $new_post            = isset( $_POST['new_post'] ) ? json_decode( base64_decode( wp_unslash( $_POST['new_post'] ) ), true ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        $post_custom         = isset( $_POST['post_custom'] ) ? json_decode( base64_decode( wp_unslash( $_POST['post_custom'] ) ), true ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        $post_category       = isset( $_POST['post_category'] ) ? rawurldecode( base64_decode( wp_unslash( $_POST['post_category'] ) ) ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        $post_tags           = isset( $new_post['post_tags'] ) ? rawurldecode( $new_post['post_tags'] ) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
        $post_featured_image = isset( $_POST['post_featured_image'] ) && ! empty( $_POST['post_featured_image'] ) ? base64_decode( wp_unslash( $_POST['post_featured_image'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        $upload_dir          = isset( $_POST['mainwp_upload_dir'] ) ? json_decode( base64_decode( wp_unslash( $_POST['mainwp_upload_dir'] ) ), true ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..

        $others = array();
        if ( isset( $_POST['featured_image_data'] ) ) {
            $others['featured_image_data'] = ! empty( $_POST['featured_image_data'] ) ? json_decode( base64_decode( wp_unslash( $_POST['featured_image_data'] ) ), true ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        }
        // phpcs:enable WordPress.Security.NonceVerification
        $result = $this->create_post( $new_post, $post_custom, $post_category, $post_featured_image, $upload_dir, $post_tags, $others );

        if ( is_array( $result ) && isset( $result['error'] ) ) {
            MainWP_Helper::instance()->error( $result['error'] );
        }

        $created = $result['success'];
        if ( true !== $created ) {
            MainWP_Helper::instance()->error( 'Undefined error' );
        }

        do_action( 'mainwp_child_after_newpost', $result );

        $information['added']      = true;
        $information['added_id']   = $result['added_id'];
        $information['link']       = $result['link'];
        $information['other_data'] = array(
            'new_post_data' => is_array( $result ) && isset( $result['new_post_data'] ) ? $result['new_post_data'] : array(),
        );
        MainWP_Helper::write( $information );
    }

    /**
     * Post Action.
     *
     * @uses \MainWP\Child\MainWP_Child_Links_Checker()
     * @uses \MainWP\Child\MainWP_Child_Posts::get_post_edit()
     * @uses \MainWP\Child\MainWP_Child_Posts::get_page_edit()
     * @uses \MainWP\Child\MainWP_Helper::write()
     * @uses \MainWP\Child\MainWP_Child_Links_Checker::get_class_name()
     */
    public function post_action() { //phpcs:ignore -- NOSONAR - complex.
        // phpcs:disable WordPress.Security.NonceVerification
        $action  = MainWP_System::instance()->validate_params( 'action' );
        $postId  = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
        $my_post = array();

        $old_post_type_name = '';
        $post_current       = false;
        if ( $postId ) {
            $post_current = get_post( $postId );
            if ( $post_current ) {
                $old_post_type_name = strtolower( $this->get_post_type_name( $post_current->post_type ) );
            }
        }

        if ( 'publish' === $action ) {
            if ( empty( $post_current ) ) {
                $information['status'] = 'FAIL';
            } elseif ( 'future' === $post_current->post_status ) {
                    wp_publish_post( $postId );
                    wp_update_post(
                        array(
                            'ID'            => $postId,
                            'post_date'     => current_time( 'mysql', false ),
                            'post_date_gmt' => current_time( 'mysql', true ),
                        )
                    );
            } else {
                wp_update_post(
                    array(
                        'ID'          => $postId,
                        'post_status' => 'publish',
                    )
                );
            }
        } elseif ( 'update' === $action ) {
            $postData = isset( $_POST['post_data'] ) ? wp_unslash( $_POST['post_data'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
            $my_post  = is_array( $postData ) ? $postData : array();
            wp_update_post( $my_post );
        } elseif ( 'unpublish' === $action ) {
            $my_post['ID']          = $postId;
            $my_post['post_status'] = 'draft';
            wp_update_post( $my_post );
        } elseif ( 'trash' === $action ) {
            add_action( 'trash_post', array( MainWP_Child_Links_Checker::get_class_name(), 'hook_post_deleted' ) );
            wp_trash_post( $postId );
        } elseif ( 'delete' === $action ) {
            add_action( 'delete_post', array( MainWP_Child_Links_Checker::get_class_name(), 'hook_post_deleted' ) );
            $delete_result = $this->delete_post_with_result( $postId );
            if ( ! $delete_result['deleted'] && ! $delete_result['already_absent'] ) {
                $information['status'] = 'FAIL';
            }
        } elseif ( 'restore' === $action ) {
            wp_untrash_post( $postId );
        } elseif ( 'update_meta' === $action ) {
            $values     = isset( $_POST['values'] ) ? json_decode( base64_decode( wp_unslash( $_POST['values'] ) ), true ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
            $meta_key   = $values['meta_key'];
            $meta_value = $values['meta_value'];
            $check_prev = $values['check_prev'];

            foreach ( $meta_key as $i => $key ) {
                if ( 1 === intval( $check_prev[ $i ] ) ) {
                    update_post_meta( $postId, $key, get_post_meta( $postId, $key, true ) ? get_post_meta( $postId, $key, true ) : $meta_value[ $i ] );
                } else {
                    update_post_meta( $postId, $key, $meta_value[ $i ] );
                }
            }
        } elseif ( 'get_edit' === $action ) {
            $postId    = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
            $post_type = isset( $_POST['post_type'] ) ? sanitize_text_field( wp_unslash( $_POST['post_type'] ) ) : '';
            if ( 'post' === $post_type ) {
                $my_post = $this->get_post_edit( $postId );
            } else {
                $my_post = $this->get_page_edit( $postId );
            }
        } else {
            $information['status'] = 'FAIL';
        }

        if ( ! isset( $information['status'] ) ) {
            $information['status'] = 'SUCCESS';
        }
        // phpcs:enable WordPress.Security.NonceVerification

        $post_dt = array();
        // support logging.
        if ( 'delete' === $action ) {
            if ( $post_current ) {
                $post_dt = array(
                    'post_type'     => $post_current->post_type,
                    'post_title'    => $post_current->post_title,
                    'singular_name' => $old_post_type_name,
                );
            }
        } else {
            $post_updated = get_post( $postId );
            if ( $post_updated ) {
                $post_type_name = strtolower( $this->get_post_type_name( $post_updated->post_type ) );
                $post_dt        = array(
                    'post_id'       => $postId,
                    'post_type'     => $post_updated->post_type,
                    'post_title'    => $post_updated->post_title,
                    'post_date'     => $post_updated->post_date,
                    'post_date_gmt' => $post_updated->post_date_gmt,
                    'new_status'    => $post_updated->post_status,
                    'old_status'    => $post_current->post_status,
                    'singular_name' => $post_type_name,
                );
            }
        }

        $information['other_data'] = array(
            'post_action_data' => $post_dt,
        );
        $information['my_post']    = $my_post;
        MainWP_Helper::write( $information );
    }

    /**
     * Permanently delete one exact post and prove the terminal result.
     *
     * A missing post is an idempotent terminal success. A present post is
     * successful only when WordPress reports a deleted post and a fresh read
     * proves absence. The closed result is intentionally not written directly
     * to the legacy wire response, whose established shape remains unchanged.
     *
     * @param mixed $post_id Candidate post identity.
     * @return array{deleted:bool,already_absent:bool}
     */
    public function delete_post_with_result( $post_id ) {
        if ( ! is_int( $post_id ) && ! ( is_string( $post_id ) && 1 === preg_match( '/^[1-9][0-9]*$/D', $post_id ) ) ) {
            return array(
                'deleted'        => false,
                'already_absent' => false,
            );
        }

        $post_id = (int) $post_id;
        if ( 1 > $post_id ) {
            return array(
                'deleted'        => false,
                'already_absent' => false,
            );
        }

        if ( null === get_post( $post_id ) ) {
            return array(
                'deleted'        => false,
                'already_absent' => true,
            );
        }

        $deleted = wp_delete_post( $post_id, true );
        return array(
            'deleted'        => $deleted instanceof \WP_Post && null === get_post( $post_id ),
            'already_absent' => false,
        );
    }

    /**
     * Gets the singular post type label
     *
     * @param string $post_type_slug Post type slug.
     *
     * @return string Post type label
     */
    public function get_post_type_name( $post_type_slug ) {
        $name = esc_html__( 'Post', 'mainwp-child' ); // Default.

        if ( post_type_exists( $post_type_slug ) ) {
            $post_type = get_post_type_object( $post_type_slug );
            $name      = $post_type->labels->singular_name;
        }

        return $name;
    }

    /**
     * Get post edit data.
     *
     * @param string $id Post ID.
     *
     * @return array|bool Return $post_data or FALSE on failure.
     */
    private function get_post_edit( $id ) { //phpcs:ignore -- NOSONAR - complex.
        $post = get_post( $id );
        if ( $post ) {
            $categoryObjects = get_the_category( $post->ID );
            $categories      = '';
            foreach ( $categoryObjects as $cat ) {
                if ( '' !== $categories ) {
                    $categories .= ',';
                }
                $categories .= $cat->name;
            }
            $post_category = $categories;

            $tagObjects = get_the_tags( $post->ID );
            $tags       = '';
            if ( is_array( $tagObjects ) ) {
                foreach ( $tagObjects as $tag ) {
                    if ( '' !== $tags ) {
                        $tags .= ',';
                    }
                    $tags .= $tag->name;
                }
            }
            $post_tags = $tags;

            $post_custom = get_post_custom( $id );

            $galleries           = get_post_gallery( $id, false );
            $post_gallery_images = array();

            if ( is_array( $galleries ) && isset( $galleries['ids'] ) ) {
                $attached_images = explode( ',', $galleries['ids'] );
                foreach ( $attached_images as $attachment_id ) {
                    $attachment = get_post( $attachment_id );
                    if ( $attachment ) {
                        $post_gallery_images[] = array(
                            'id'          => $attachment_id,
                            'alt'         => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ),
                            'caption'     => $attachment->post_excerpt,
                            'description' => $attachment->post_content,
                            'src'         => $attachment->guid,
                            'title'       => $attachment->post_title,
                        );
                    }
                }
            }

            include_once ABSPATH . 'wp-includes' . DIRECTORY_SEPARATOR . 'post-thumbnail-template.php'; // NOSONAR -- WP compatible.
            $post_featured_image = get_post_thumbnail_id( $id );
            $child_upload_dir    = wp_upload_dir();
            $new_post            = array(
                'edit_id'        => $id,
                'is_sticky'      => is_sticky( $id ) ? 1 : 0,
                'post_title'     => $post->post_title,
                'post_content'   => $post->post_content,
                'post_status'    => $post->post_status,
                'post_date'      => $post->post_date,
                'post_date_gmt'  => $post->post_date_gmt,
                'post_tags'      => $post_tags,
                'post_name'      => $post->post_name,
                'post_excerpt'   => $post->post_excerpt,
                'comment_status' => $post->comment_status,
                'ping_status'    => $post->ping_status,
                'post_type'      => $post->post_type,
                'post_password'  => $post->post_password,
            );

            if ( ! empty( $post_featured_image ) ) { // Featured image is set, retrieve URL.
                $img                 = wp_get_attachment_image_src( $post_featured_image, 'full' );
                $post_featured_image = $img[0];
            }

            require_once ABSPATH . 'wp-admin/includes/post.php'; // NOSONAR - WP compatible.
            wp_set_post_lock( $id );

            // prepare $post_custom values.
            $new_post_custom = array();
            foreach ( $post_custom as $meta_key => $meta_values ) {
                $new_meta_values = array();
                foreach ( $meta_values as $key_value => $meta_value ) {
                    if ( is_serialized( $meta_value ) ) {
                        $meta_value = unserialize( $meta_value ); // phpcs:ignore --  safe internal value.
                    }
                    $new_meta_values[ $key_value ] = $meta_value;
                }
                $new_post_custom[ $meta_key ] = $new_meta_values;
            }

            return array(
                'new_post'            => base64_encode( wp_json_encode( $new_post ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_custom'         => base64_encode( wp_json_encode( $new_post_custom ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_category'       => base64_encode( $post_category ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_featured_image' => base64_encode( $post_featured_image ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_gallery_images' => base64_encode( wp_json_encode( $post_gallery_images ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'child_upload_dir'    => base64_encode( wp_json_encode( $child_upload_dir ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
            );
        }
        return false;
    }

    /**
     * Get page edit data.
     *
     * @param string $id Page ID.
     *
     * @return array|bool Return $post_data or FALSE on failure.
     */
    private function get_page_edit( $id ) { //phpcs:ignore -- NOSONAR - complex.
        $post = get_post( $id );
        if ( $post ) {
            $post_custom = get_post_custom( $id );
            include_once ABSPATH . 'wp-includes' . DIRECTORY_SEPARATOR . 'post-thumbnail-template.php'; // NOSONAR -- WP compatible.
            $post_featured_image = get_post_thumbnail_id( $id );
            $child_upload_dir    = wp_upload_dir();

            $new_post = array(
                'edit_id'        => $id,
                'post_title'     => $post->post_title,
                'post_content'   => $post->post_content,
                'post_status'    => $post->post_status,
                'post_date'      => $post->post_date,
                'post_date_gmt'  => $post->post_date_gmt,
                'post_type'      => 'page',
                'post_name'      => $post->post_name,
                'post_excerpt'   => $post->post_excerpt,
                'comment_status' => $post->comment_status,
                'ping_status'    => $post->ping_status,
                'post_password'  => $post->post_password,
            );

            if ( ! empty( $post_featured_image ) ) {
                    $img                 = wp_get_attachment_image_src( $post_featured_image, 'full' );
                    $post_featured_image = $img[0];
            }

            $galleries           = get_post_gallery( $id, false );
            $post_gallery_images = array();

            if ( is_array( $galleries ) && isset( $galleries['ids'] ) ) {
                $attached_images = explode( ',', $galleries['ids'] );
                foreach ( $attached_images as $attachment_id ) {
                    $attachment = get_post( $attachment_id );
                    if ( $attachment ) {
                        $post_gallery_images[] = array(
                            'id'          => $attachment_id,
                            'alt'         => get_post_meta( $attachment->ID, '_wp_attachment_image_alt', true ),
                            'caption'     => $attachment->post_excerpt,
                            'description' => $attachment->post_content,
                            'src'         => $attachment->guid,
                            'title'       => $attachment->post_title,
                        );
                    }
                }
            }

            require_once ABSPATH . 'wp-admin/includes/post.php'; // NOSONAR - WP compatible.
            wp_set_post_lock( $id );

            // prepare $post_custom values.
            $new_post_custom = array();
            foreach ( $post_custom as $meta_key => $meta_values ) {
                $new_meta_values = array();
                foreach ( $meta_values as $key_value => $meta_value ) {
                    if ( is_serialized( $meta_value ) ) {
                        $meta_value = unserialize( $meta_value ); // phpcs:ignore -- safe internal value.
                    }
                    $new_meta_values[ $key_value ] = $meta_value;
                }
                $new_post_custom[ $meta_key ] = $new_meta_values;
            }

            return array(
                'new_post'            => base64_encode( wp_json_encode( $new_post ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_custom'         => base64_encode( wp_json_encode( $new_post_custom ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_featured_image' => base64_encode( $post_featured_image ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'post_gallery_images' => base64_encode( wp_json_encode( $post_gallery_images ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                'child_upload_dir'    => base64_encode( wp_json_encode( $child_upload_dir ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
            );
        }
        return false;
    }

    /**
     * Create new post.
     *
     * @param array  $new_post            Post data array.
     * @param array  $post_custom         Post custom meta data.
     * @param string $post_category       Post categories.
     * @param string $post_featured_image Post featured image.
     * @param string $upload_dir          Upload directory.
     * @param string $post_tags           Post tags.
     * @param array  $others              Other data.
     *
     * @return array|string[] $ret Return success array, permalink & Post ID.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::set_post_custom_data()
     * @uses \MainWP\Child\MainWP_Child_Posts::update_found_images()
     * @uses \MainWP\Child\MainWP_Child_Posts::create_has_shortcode_gallery()
     * @uses \MainWP\Child\MainWP_Child_Posts::create_post_plus()
     * @uses \MainWP\Child\MainWP_Child_Posts::update_post_data()
     */
    private function create_post( //phpcs:ignore -- NOSONAR - complex.
        $new_post,
        $post_custom,
        $post_category,
        $post_featured_image,
        $upload_dir,
        $post_tags,
        $others = array()
    ) {

        /**
        * Hook: `mainwp_before_post_update`
        *
        * Runs before creating or updating a post via MainWP dashboard.
        *
        * @param array  $new_post      � Post data array.
        * @param array  $post_custom   � Post custom meta data.
        * @param string $post_category � Post categories.
        * @param string $post_tags     � Post tags.
        */
        do_action( 'mainwp_before_post_update', $new_post, $post_custom, $post_category, $post_tags, $others );

        $edit_post_id = 0;
        $is_post_plus = false;

        $this->set_post_custom_data( $new_post, $post_custom, $post_tags, $edit_post_id, $is_post_plus, $others );

        require_once ABSPATH . 'wp-admin/includes/post.php'; // NOSONAR - WP compatible.

        if ( $edit_post_id ) {
            $user_id = wp_check_post_lock( $edit_post_id );
            if ( $user_id ) {
                $user = get_userdata( $user_id );
                // translators: %s: User display name.
                $error = sprintf( esc_html__( 'This content is currently locked. %s is currently editing.', 'mainwp-child' ), $user->display_name );
                return array( 'error' => $error );
            }
        }

        // if editing post then will check if image existed.
        $check_image_existed = $edit_post_id ? true : false;

        $this->update_found_images( $new_post, $upload_dir, $check_image_existed );
        $this->create_has_shortcode_gallery( $new_post );

        if ( $is_post_plus ) {
            $this->create_post_plus( $new_post, $post_custom );
        }

        if ( isset( $post_custom['_mainwp_replace_advance_img'] ) && ! empty( $post_custom['_mainwp_replace_advance_img'][0] ) ) {
            $new_post['post_content'] = static::replace_advanced_image( $new_post['post_content'], $upload_dir );
            $new_post['post_content'] = static::replace_advanced_image( $new_post['post_content'], $upload_dir, true ); // to fix images url with slashes.
            unset( $post_custom['_mainwp_replace_advance_img'] );
        }

        // Save the post to the WP.
        remove_filter( 'content_save_pre', 'wp_filter_post_kses' );  // to fix brake scripts or html.
        $post_status             = $new_post['post_status']; // save post_status.
        $new_post['post_status'] = 'auto-draft'; // to fix reports, to log as created post.

        $current_post = false;
        // Update post.
        if ( $edit_post_id ) {
            // check if post existed.
            $current_post = get_post( $edit_post_id );
            if ( $current_post && ( ( ! isset( $new_post['post_type'] ) && 'post' === $current_post->post_type ) || ( isset( $new_post['post_type'] ) && $new_post['post_type'] === $current_post->post_type ) ) ) {
                $new_post['ID'] = $edit_post_id;
            }
            $new_post['post_status'] = $post_status; // child reports: to logging as update post.
        }
        $wp_error = false;
        if ( $edit_post_id ) {
            $new_post_id = wp_update_post( $new_post, $wp_error ); // to fix: update post.
        } else {
            $new_post_id = wp_insert_post( $new_post, $wp_error ); // insert post.
        }
        // Show errors if something went wrong.
        if ( is_wp_error( $wp_error ) ) {
            return $wp_error->get_error_message();
        }
        if ( empty( $new_post_id ) ) {
            return array( 'error' => 'Empty post id' );
        }

        if ( ! $edit_post_id ) {
            wp_update_post(
                array(
                    'ID'          => $new_post_id,
                    'post_status' => $post_status,
                )
            );
        }

        if ( is_array( $post_custom ) && isset( $post_custom['_mainwp_edit_post_save_to_post_type'] ) ) {
            $saved_post_type = $post_custom['_mainwp_edit_post_save_to_post_type'];
            $saved_post_type = is_array( $saved_post_type ) ? current( $saved_post_type ) : $saved_post_type;
            if ( ! empty( $saved_post_type ) ) {
                wp_update_post(
                    array(
                        'ID'        => $new_post_id,
                        'post_type' => $saved_post_type,
                    )
                );
            }
            unset( $post_custom['_mainwp_edit_post_save_to_post_type'] );
        }

        $this->update_post_data( $new_post_id, $post_custom, $post_category, $post_featured_image, $check_image_existed, $is_post_plus, $others );

        // unlock if edit post.
        if ( $edit_post_id ) {
            update_post_meta( $edit_post_id, '_edit_lock', '' );
        }

        $post_added = get_post( $new_post_id );
        $post_dt    = array();
        if ( $post_added ) {
            $post_type_name = strtolower( $this->get_post_type_name( $post_added->post_type ) );
            $post_dt        = array(
                'post_id'       => $new_post_id,
                'post_type'     => $post_added->post_type,
                'post_title'    => $post_added->post_title,
                'post_date'     => $post_added->post_date,
                'post_date_gmt' => $post_added->post_date_gmt,
                'new_status'    => $post_added->post_status,
                'old_status'    => $current_post ? $current_post->post_status : '',
                'singular_name' => $post_type_name,
                'is_editing'    => $edit_post_id ? 1 : 0,
            );
        }

        $result                  = array();
        $result['success']       = true;
        $result['link']          = get_permalink( $new_post_id );
        $result['added_id']      = $new_post_id;
        $result['new_post_data'] = $post_dt;
        return $result;
    }

    /**
     * Set custom post data.
     *
     * @param array  $new_post            Post data array.
     * @param array  $post_custom         Post custom meta data.
     * @param string $post_tags           Post tags.
     * @param string $edit_post_id        Edit Post ID.
     * @param bool   $is_post_plus        TRUE|FALSE, Whether or not this came from MainWP Post Plus Extension.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::update_wp_rocket_custom_post()
     */
    private function set_post_custom_data( &$new_post, $post_custom, $post_tags, &$edit_post_id, &$is_post_plus ) { //phpcs:ignore -- NOSONAR - complex.

        /**
         * Current user global.
         *
         * @global string
         */
        global $current_user;

        $this->update_wp_rocket_custom_post( $post_custom );

        // current user may be connected admin or alternative admin.
        $current_uid = $current_user->ID;

        // Set up a new post (adding additional information).
        $new_post['post_author'] = isset( $new_post['post_author'] ) && ! empty( $new_post['post_author'] ) ? $new_post['post_author'] : $current_uid;

        if ( isset( $new_post['post_title'] ) ) {
            $new_post['post_title'] = MainWP_Utility::esc_content( $new_post['post_title'], 'mixed' );
        }

        if ( isset( $new_post['post_excerpt'] ) ) {
            $new_post['post_excerpt'] = MainWP_Utility::esc_content( $new_post['post_excerpt'], 'mixed' );
        }

        if ( isset( $new_post['custom_post_author'] ) && ! empty( $new_post['custom_post_author'] ) ) {
            $_author = get_user_by( 'login', $new_post['custom_post_author'] );
            if ( ! empty( $_author ) ) {
                $new_post['post_author'] = $_author->ID;
            }
            unset( $new_post['custom_post_author'] );
        }

        // post plus extension process.
        $is_post_plus = isset( $post_custom['_mainwp_post_plus'] ) ? true : false;

        if ( $is_post_plus && isset( $new_post['post_date_gmt'] ) && ! empty( $new_post['post_date_gmt'] ) && '0000-00-00 00:00:00' !== $new_post['post_date_gmt'] ) {
            $post_date_timestamp   = strtotime( $new_post['post_date_gmt'] ) + get_option( 'gmt_offset' ) * 60 * 60;
            $new_post['post_date'] = date( 'Y-m-d H:i:s', $post_date_timestamp ); // phpcs:ignore -- local time.
        }

        if ( isset( $post_tags ) && '' !== $post_tags ) {
            $new_post['tags_input'] = $post_tags;
        }

        if ( isset( $post_custom['_mainwp_edit_post_id'] ) && $post_custom['_mainwp_edit_post_id'] ) {
            $edit_post_id = current( $post_custom['_mainwp_edit_post_id'] );
        } elseif ( isset( $new_post['ID'] ) && $new_post['ID'] ) {
            $edit_post_id = $new_post['ID'];
        }
    }

    /**
     * Update post data.
     *
     * @param string $new_post_id         New post ID.
     * @param array  $post_custom         Post custom meta data.
     * @param string $post_category       Post categories.
     * @param string $post_featured_image Post featured image.
     * @param bool   $check_image_existed TRUE|FALSE, Whether or not featured image already exists.
     * @param bool   $is_post_plus        TRUE|FALSE, Whether or not this came from MainWP Post Plus Extension.
     * @param array  $others        Others data.
     *
     * @uses \MainWP\Child\MainWP_Child_Posts::set_custom_post_fields()
     * @uses \MainWP\Child\MainWP_Child_Posts::update_seo_meta()
     * @uses \MainWP\Child\MainWP_Child_Posts::create_set_categories()
     * @uses \MainWP\Child\MainWP_Child_Posts::create_featured_image()
     * @uses \MainWP\Child\MainWP_Child_Posts::post_plus_update_author()
     * @uses \MainWP\Child\MainWP_Child_Posts::post_plus_update_categories()
     */
    private function update_post_data( $new_post_id, $post_custom, $post_category, $post_featured_image, $check_image_existed, $is_post_plus, $others ) {
        unset( $check_image_existed );
        $seo_ext_activated = false;
        if ( class_exists( '\WPSEO_Meta' ) && class_exists( '\WPSEO_Admin' ) ) {
            $seo_ext_activated = true;
        }

        $post_to_only_existing_categories = false;

        $this->set_custom_post_fields( $new_post_id, $post_custom, $seo_ext_activated, $post_to_only_existing_categories );

        // yoast seo plugin activated.
        if ( $seo_ext_activated ) {
            $this->update_seo_meta( $new_post_id, $post_custom );
        }

        $this->create_set_categories( $new_post_id, $post_category, $post_to_only_existing_categories );
        $this->create_featured_image( $new_post_id, $post_featured_image, true, $others ); // always checks featured img.

        // post plus extension process.
        if ( $is_post_plus ) {
            $this->post_plus_update_author( $new_post_id, $post_custom );
            $this->post_plus_update_categories( $new_post_id, $post_custom );
        }

        // to support custom post author.
        $custom_post_author = apply_filters( 'mainwp_create_post_custom_author', false, $new_post_id );
        if ( ! empty( $custom_post_author ) ) {
            wp_update_post(
                array(
                    'ID'          => $new_post_id,
                    'post_author' => $custom_post_author,
                )
            );
        }
    }

    /**
     * Update WPRocket custom post.
     *
     * @param array $post_custom Post custom meta data.
     *
     * @uses \get_rocket_option()
     * @see https://github.com/wp-media/wp-rocket/blob/master/inc/functions/options.php
     *
     * @uses \MainWP\Child\MainWP_Child_WP_Rocket::instance()::is_activated()
     * @uses \MainWP\Child\MainWP_Child_WP_Rocket::is_activated()
     */
    private function update_wp_rocket_custom_post( &$post_custom ) { //phpcs:ignore -- NOSONAR - complex.
        // Options fields.
        $wprocket_fields = array(
            'lazyload',
            'lazyload_iframes',
            'minify_html',
            'minify_css',
            'minify_js',
            'cdn',
            'async_css',
            'defer_all_js',
        );

        $wprocket_activated = false;
        if ( MainWP_Child_WP_Rocket::instance()->is_activated() && function_exists( '\get_rocket_option' ) ) {
            $wprocket_activated = true;
            foreach ( $wprocket_fields as $field ) {
                if ( ! isset( $post_custom[ '_rocket_exclude_' . $field ] ) && ! \get_rocket_option( $field ) ) {
                    $post_custom[ '_rocket_exclude_' . $field ] = array( true );
                }
            }
        }
        if ( ! $wprocket_activated ) {
            foreach ( $wprocket_fields as $field ) {
                if ( isset( $post_custom[ '_rocket_exclude_' . $field ] ) ) {
                    unset( $post_custom[ '_rocket_exclude_' . $field ] );
                }
            }
        }
    }

    /**
     * Search for all the images added to the new post.
     *
     * @param array  $new_post            Post data array.
     * @param string $upload_dir          Upload directory.
     * @param bool   $check_image_existed TRUE|FALSE, Whether or not featured image already exists.
     *
     * @uses \MainWP\Child\MainWP_Utility::upload_image()
     * @uses \MainWP\Child\MainWP_Helper::log_debug()
     */
    private function update_found_images( &$new_post, $upload_dir, $check_image_existed ) { //phpcs:ignore -- NOSONAR - complex.

        // Some images have a href tag to click to navigate to the image.. we need to replace this too.
        $foundMatches = preg_match_all( '/(<a[^>]+href=\"(.*?)\"[^>]*>)?(<img[^>\/]*src=\"((.*?)(png|gif|jpg|jpeg|avif))\")/ix', $new_post['post_content'], $matches, PREG_SET_ORDER );

        if ( $foundMatches > 0 ) {
            // We found images, now to download them so we can start balbal.
            foreach ( $matches as $match ) {
                $hrefLink = $match[2];
                $imgUrl   = $match[4];

                if ( ! isset( $upload_dir['baseurl'] ) || ( false === strripos( $imgUrl, $upload_dir['baseurl'] ) ) ) { // url of image is not in dashboard site.
                    continue;
                }

                if ( preg_match( '/-\d{3}x\d{3}\.[a-zA-Z0-9]{3,4}$/', $imgUrl, $imgMatches ) ) {
                    $search         = $imgMatches[0];
                    $replace        = '.' . $match[6];
                    $originalImgUrl = str_replace( $search, $replace, $imgUrl );
                } else {
                    $originalImgUrl = $imgUrl;
                }

                try {
                    $downloadfile      = MainWP_Utility::upload_image( $originalImgUrl, array(), $check_image_existed );
                    $localUrl          = $downloadfile['url'];
                    $linkToReplaceWith = dirname( $localUrl );
                    if ( '' !== $hrefLink ) {
                        $server     = MainWP_Child_Keys_Manager::get_encrypted_option( 'mainwp_child_server' );
                        $serverHost = wp_parse_url( $server, PHP_URL_HOST );
                        if ( ! empty( $serverHost ) && strpos( $hrefLink, $serverHost ) !== false ) {
                            $serverHref               = 'href="' . $serverHost;
                            $replaceServerHref        = 'href="' . wp_parse_url( $localUrl, PHP_URL_SCHEME ) . '://' . wp_parse_url( $localUrl, PHP_URL_HOST );
                            $new_post['post_content'] = str_replace( $serverHref, $replaceServerHref, $new_post['post_content'] );
                        }
                    }
                    $lnkToReplace = dirname( $imgUrl );
                    if ( 'http:' !== $lnkToReplace && 'https:' !== $lnkToReplace ) {
                        $new_post['post_content'] = str_replace( $imgUrl, $localUrl, $new_post['post_content'] ); // replace src image.
                        $new_post['post_content'] = str_replace( $lnkToReplace, $linkToReplaceWith, $new_post['post_content'] );
                    }
                } catch ( MainWP_Exception $e ) {
                    MainWP_Helper::log_debug( $e->getMessage() );
                }
            }
        }
    }

    /**
     * Method replace_advanced_image()
     *
     * Handle upload advanced image.
     *
     * @param array $content post content data.
     * @param array $upload_dir upload directory info.
     * @param bool  $withslashes to use preg pattern with slashes.
     *
     * @return mixed array of result.
     */
    public static function replace_advanced_image( $content, $upload_dir, $withslashes = false ) { //phpcs:ignore -- NOSONAR - complex.

        if ( empty( $upload_dir ) || ! isset( $upload_dir['baseurl'] ) ) {
            return $content;
        }

        $dashboard_url = MainWP_Child_Keys_Manager::get_encrypted_option( 'mainwp_child_server' );

        $site_url_destination = get_site_url();

        // to fix url with slashes.
        if ( $withslashes ) {
            $site_url_destination = str_replace( '/', '\/', $site_url_destination );
            $dashboard_url        = str_replace( '/', '\/', $dashboard_url );
        }

        $foundMatches = preg_match_all( '#(' . preg_quote( $site_url_destination, null ) . ')[^\.]*(\.(png|gif|jpg|jpeg|avif))#ix', $content, $matches, PREG_SET_ORDER );

        if ( 0 < $foundMatches ) {

            $matches_checked = array();
            $check_double    = array();
            foreach ( $matches as $match ) {
                // to avoid double images.
                if ( ! in_array( $match[0], $check_double ) ) {
                    $check_double[]    = $match[0];
                    $matches_checked[] = $match;
                }
            }
            foreach ( $matches_checked as $match ) {

                $imgUrl = $match[0];
                if ( false === strripos( wp_unslash( $imgUrl ), $upload_dir['baseurl'] ) ) {
                    continue;
                }

                if ( preg_match( '/-\d{3}x\d{3}\.[a-zA-Z0-9]{3,4}$/', $imgUrl, $imgMatches ) ) {
                    $search         = $imgMatches[0];
                    $replace        = '.' . $match[3];
                    $originalImgUrl = str_replace( $search, $replace, $imgUrl );
                } else {
                    $originalImgUrl = $imgUrl;
                }

                try {
                    $downloadfile      = MainWP_Utility::upload_image( wp_unslash( $originalImgUrl ), array(), true );
                    $localUrl          = $downloadfile['url'];
                    $linkToReplaceWith = dirname( $localUrl );
                    $lnkToReplace      = dirname( $imgUrl );
                    if ( 'http:' !== $lnkToReplace && 'https:' !== $lnkToReplace ) {
                        $content = str_replace( $imgUrl, $localUrl, $content ); // replace src image.
                        $content = str_replace( $lnkToReplace, $linkToReplaceWith, $content );
                    }
                } catch ( MainWP_Exception $e ) {
                    // ok.
                }
            }
            if ( false === strripos( $dashboard_url, $site_url_destination ) ) {
                // replace other images src outside dashboard upload folder.
                $content = str_replace( $dashboard_url, $site_url_destination, $content );
            }
        }
        return $content;
    }

    /**
     * Create shortcode image gallery.
     *
     * @param array $new_post Post data array.
     *
     * @uses \MainWP\Child\MainWP_Utility::upload_image()
     * @uses MainWP_Exception()
     */
    private function create_has_shortcode_gallery( &$new_post ) { //phpcs:ignore -- NOSONAR - complex.

        if ( has_shortcode( $new_post['post_content'], 'gallery' ) && preg_match_all( '/\[gallery[^\]]+ids=\"(.*?)\"[^\]]*\]/ix', $new_post['post_content'], $matches, PREG_SET_ORDER ) ) {
            $replaceAttachedIds = array();
            // phpcs:disable WordPress.Security.NonceVerification
            if ( isset( $_POST['post_gallery_images'] ) ) {
                $post_gallery_images = isset( $_POST['post_gallery_images'] ) ? json_decode( base64_decode( wp_unslash( $_POST['post_gallery_images'] ) ), true ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                if ( is_array( $post_gallery_images ) ) {
                    foreach ( $post_gallery_images as $gallery ) {
                        if ( isset( $gallery['src'] ) ) {
                            try {
                                $upload = MainWP_Utility::upload_image( $gallery['src'], $gallery ); // Upload image to WP.
                                if ( null !== $upload ) {
                                    $replaceAttachedIds[ $gallery['id'] ] = $upload['id'];
                                }
                            } catch ( MainWP_Exception $e ) {
                                // ok!
                            }
                        }
                    }
                }
            }
            // phpcs:enable WordPress.Security.NonceVerification
            if ( ! empty( $replaceAttachedIds ) ) {
                foreach ( $matches as $match ) {
                    $idsToReplace     = $match[1];
                    $idsToReplaceWith = '';
                    $originalIds      = explode( ',', $idsToReplace );
                    foreach ( $originalIds as $attached_id ) {
                        if ( ! empty( $originalIds ) && isset( $replaceAttachedIds[ $attached_id ] ) ) {
                            $idsToReplaceWith .= $replaceAttachedIds[ $attached_id ] . ',';
                        }
                    }
                    $idsToReplaceWith = rtrim( $idsToReplaceWith, ',' );
                    if ( ! empty( $idsToReplaceWith ) ) {
                        $new_post['post_content'] = str_replace( '"' . $idsToReplace . '"', '"' . $idsToReplaceWith . '"', $new_post['post_content'] );
                    }
                }
            }
        }
    }

    /**
     * Create post plus post.
     *
     * @param array $new_post    Post data array.
     * @param array $post_custom Post custom meta data.
     */
    private function create_post_plus( &$new_post, $post_custom ) { //phpcs:ignore -- NOSONAR - complex.
        $random_publish_date = isset( $post_custom['_saved_draft_random_publish_date'] ) ? $post_custom['_saved_draft_random_publish_date'] : false;
        $random_publish_date = is_array( $random_publish_date ) ? current( $random_publish_date ) : null;

        if ( ! empty( $random_publish_date ) ) {
            $random_date_from = isset( $post_custom['_saved_draft_publish_date_from'] ) ? $post_custom['_saved_draft_publish_date_from'] : 0;
            $random_date_from = is_array( $random_date_from ) ? current( $random_date_from ) : 0;

            $random_date_to = isset( $post_custom['_saved_draft_publish_date_to'] ) ? $post_custom['_saved_draft_publish_date_to'] : 0;
            $random_date_to = is_array( $random_date_to ) ? current( $random_date_to ) : 0;

            $now = time();

            if ( empty( $random_date_from ) ) {
                $random_date_from = $now;
            }

            if ( empty( $random_date_to ) ) {
                $random_date_to = $now;
            }

            if ( $random_date_from === $now && $random_date_from === $random_date_to ) {
                $random_date_to = $now + 7 * 24 * 3600;
            }

            if ( $random_date_from > $random_date_to ) {
                $tmp              = $random_date_from;
                $random_date_from = $random_date_to;
                $random_date_to   = $tmp;
            }

            $random_timestamp      = wp_rand( $random_date_from, $random_date_to );
            $new_post['post_date'] = date( 'Y-m-d H:i:s', $random_timestamp ); // phpcs:ignore -- local time.
        }
    }

    /**
     * Update post plus author.
     *
     * @param string $new_post_id New post ID.
     * @param array  $post_custom Post custom meta data.
     */
    private function post_plus_update_author( $new_post_id, $post_custom ) {
        $random_privelege      = isset( $post_custom['_saved_draft_random_privelege'] ) ? $post_custom['_saved_draft_random_privelege'] : null;
        $random_privelege      = is_array( $random_privelege ) ? current( $random_privelege ) : null;
        $random_privelege_base = base64_decode( $random_privelege ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
        $random_privelege      = json_decode( $random_privelege_base, true );

        if ( is_array( $random_privelege ) && count( $random_privelege ) > 0 ) {
            $random_post_authors = array();
            foreach ( $random_privelege as $role ) {
                $users = get_users( array( 'role' => $role ) );
                foreach ( $users as $user ) {
                    $random_post_authors[] = $user->ID;
                }
            }
            if ( ! empty( $random_post_authors ) ) {
                shuffle( $random_post_authors );
                $key = array_rand( $random_post_authors );
                wp_update_post(
                    array(
                        'ID'          => $new_post_id,
                        'post_author' => $random_post_authors[ $key ],
                    )
                );
            }
        }
    }

    /**
     * Update post plus categories.
     *
     * @param string $new_post_id New post ID.
     * @param array  $post_custom Post custom meta data.
     */
    private function post_plus_update_categories( $new_post_id, $post_custom ) {
        $random_category = isset( $post_custom['_saved_draft_random_category'] ) ? $post_custom['_saved_draft_random_category'] : false;
        $random_category = is_array( $random_category ) ? current( $random_category ) : null;
        if ( ! empty( $random_category ) ) {
            $cats        = get_categories(
                array(
                    'type'       => 'post',
                    'hide_empty' => 0,
                )
            );
            $random_cats = array();
            if ( is_array( $cats ) ) {
                foreach ( $cats as $cat ) {
                    $random_cats[] = $cat->term_id;
                }
            }
            if ( ! empty( $random_cats ) ) {
                shuffle( $random_cats );
                $key = array_rand( $random_cats );
                wp_set_post_categories( $new_post_id, array( $random_cats[ $key ] ), false );
            }
        }
    }

    /**
     * Create new and set categories.
     *
     * @param string $new_post_id   New post ID.
     * @param string $post_category Post category.
     * @param bool   $post_to_only  TRUE|FALSE, Whether or not to post only to this category.
     *
     * @uses wp_create_categories() Create categories for the given post.
     * @see https://developer.wordpress.org/reference/functions/wp_create_categories/
     *
     * @uses wp_set_post_categories() Set categories for a post.
     * @see https://developer.wordpress.org/reference/functions/wp_set_post_categories/
     */
    private function create_set_categories( $new_post_id, $post_category, $post_to_only ) { //phpcs:ignore -- NOSONAR - complex.

        // If categories exist, create them (second parameter of wp_create_categories adds the categories to the post).
        include_once ABSPATH . 'wp-admin/includes/taxonomy.php'; // // NOSONAR -- WP compatible. Contains wp_create_categories.
        if ( isset( $post_category ) && '' !== $post_category ) {
            $categories = explode( ',', $post_category );
            if ( count( $categories ) > 0 ) {
                if ( ! $post_to_only ) {
                    $post_category = wp_create_categories( $categories, $new_post_id );
                } else {
                    $cat_ids = array();
                    foreach ( $categories as $cat ) {
                        $id = category_exists( $cat );
                        if ( $id ) {
                            $cat_ids[] = $id;
                        }
                    }
                    if ( ! empty( $cat_ids ) ) {
                        wp_set_post_categories( $new_post_id, $cat_ids );
                    }
                }
            }
        }
    }

    /**
     * Set custom post fields.
     *
     * @param string $new_post_id       New post ID.
     * @param array  $post_custom       Post custom meta data.
     * @param bool   $seo_ext_activated TRUE|FALSE, Whether or not Yoast SEO is activateed or not.
     * @param bool   $post_to_only      TRUE|FALSE, Whether or not to post only to this category.
     */
    private function set_custom_post_fields( $new_post_id, $post_custom, $seo_ext_activated, &$post_to_only ) { //phpcs:ignore -- NOSONAR - complex.

        // Set custom fields.
        $not_allowed = array(
            '_slug',
            '_tags',
            '_edit_lock',
            '_selected_sites',
            '_selected_groups',
            '_selected_by',
            '_categories',
            '_edit_last',
            '_sticky',
            '_mainwp_post_dripper',
            '_bulkpost_do_not_del',
            '_mainwp_spin_me',
            '_mainwp_boilerplate_sites_posts',
            '_mainwp_boilerplate',
            '_mainwp_post_plus',
            '_saved_as_draft',
            '_saved_draft_categories',
            '_saved_draft_tags',
            '_saved_draft_random_privelege',
            '_saved_draft_random_category',
            '_saved_draft_random_publish_date',
            '_saved_draft_publish_date_from',
            '_saved_draft_publish_date_to',
            '_post_to_only_existing_categories',
            '_mainwp_edit_post_site_id',
            '_mainwp_edit_post_id',
            '_edit_post_status',
            '_mainwp_edit_post_type',
            '_mainwp_edit_post_status',
            '_mainwp_edit_post_save_to_post_type',
            '_mainwp_post_dripper_sites_number',
            '_mainwp_post_dripper_time_number',
            '_mainwp_post_dripper_select_time',
            '_mainwp_post_dripper_use_post_dripper',
            'mainwp_post_id',
            '_mainwp_post_dripper_selected_drip_sites',
            '_mainwp_post_dripper_total_drip_sites',
            '_mainwp_replace_advance_img',
        );

        if ( $seo_ext_activated ) {
            // update those custom fields later.
            $not_allowed[] = \WPSEO_Meta::$meta_prefix . 'opengraph-image-id';
            $not_allowed[] = \WPSEO_Meta::$meta_prefix . 'opengraph-image';
        }

        if ( is_array( $post_custom ) ) {
            foreach ( $post_custom as $meta_key => $meta_values ) {
                if ( ! in_array( $meta_key, $not_allowed ) ) {
                    foreach ( $meta_values as $meta_value ) {
                        if ( 0 === strpos( $meta_key, '_mainwp_spinner_' ) ) {
                            continue;
                        }

                        if ( ! $seo_ext_activated ) {
                            // if WordPress SEO plugin is not activated do not save yoast post meta.
                            if ( false === strpos( $meta_key, '_yoast_wpseo_' ) ) {
                                update_post_meta( $new_post_id, $meta_key, $meta_value );
                            }
                        } else {
                            update_post_meta( $new_post_id, $meta_key, $meta_value );
                        }
                    }
                } elseif ( '_sticky' === $meta_key ) {
                    foreach ( $meta_values as $meta_value ) {
                        if ( 'sticky' === base64_decode( $meta_value ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions -- base64_encode function is used for http encode compatible..
                            stick_post( $new_post_id );
                        }
                    }
                } elseif ( '_post_to_only_existing_categories' === $meta_key ) {
                    if ( isset( $meta_values[0] ) && $meta_values[0] ) {
                        $post_to_only = true;
                    }
                }
            }
        }
    }

    /**
     * Update Yoast SEO Extension meta.
     *
     * @param string $new_post_id     New post ID.
     * @param array  $post_custom      Post custom meta data.
     *
     * @uses \MainWP\Child\MainWP_Utility::upload_image()
     * @uses \WPSEO_Meta::$meta_prefix()
     * @uses MainWP_Exception()
     */
    private function update_seo_meta( $new_post_id, $post_custom ) {

        $_seo_opengraph_image = isset( $post_custom[ \WPSEO_Meta::$meta_prefix . 'opengraph-image' ] ) ? $post_custom[ \WPSEO_Meta::$meta_prefix . 'opengraph-image' ] : array();
        $_seo_opengraph_image = current( $_seo_opengraph_image );
        $_server_domain       = '';
        $_server              = MainWP_Child_Keys_Manager::get_encrypted_option( 'mainwp_child_server' );
        if ( preg_match( '/(https?:\/\/[^\/]+\/).+/', $_server, $matchs ) ) {
            $_server_domain = isset( $matchs[1] ) ? $matchs[1] : '';
        }

        // upload image if it on the server.
        if ( ! empty( $_seo_opengraph_image ) && false !== strpos( $_seo_opengraph_image, $_server_domain ) ) {
            try {
                $upload = MainWP_Utility::upload_image( $_seo_opengraph_image ); // Upload image to WP.
                if ( null !== $upload ) {
                    update_post_meta( $new_post_id, \WPSEO_Meta::$meta_prefix . 'opengraph-image', $upload['url'] ); // Add the image to the post!
                    update_post_meta( $new_post_id, \WPSEO_Meta::$meta_prefix . 'opengraph-image-id', $upload['id'] ); // Add the id image to the post!
                }
            } catch ( MainWP_Exception $e ) {
                // ok!
            }
        }
    }

    /**
     * Create featured image.
     *
     * @param string $new_post_id         New post ID.
     * @param string $post_featured_image Post featured image.
     * @param bool   $check_image_existed TRUE|FALSE, Whether or not featured image already exists.
     * @param array  $others        Post custom others meta data.
     *
     * @uses \MainWP\Child\MainWP_Utility::upload_image()
     * @uses \Excepsion()
     */
    private function create_featured_image( $new_post_id, $post_featured_image, $check_image_existed, $others ) {

        $featured_image_exist = false;
        // If featured image exists - set it.
        if ( null !== $post_featured_image ) {
            try {
                $upload = MainWP_Utility::upload_image( $post_featured_image, array(), $check_image_existed, $new_post_id ); // Upload image to WP.
                if ( null !== $upload ) {
                    update_post_meta( $new_post_id, '_thumbnail_id', $upload['id'] ); // Add the thumbnail to the post!
                    $featured_image_exist = true;
                    if ( isset( $others['featured_image_data'] ) && ! empty( $others['featured_image_data'] ) ) {
                        $_image_data = $others['featured_image_data'];
                        update_post_meta( $upload['id'], '_wp_attachment_image_alt', $_image_data['alt'] );
                        wp_update_post(
                            array(
                                'ID'           => $upload['id'],
                                'post_excerpt' => MainWP_Utility::esc_content( $_image_data['caption'], 'mixed' ),
                                'post_content' => $_image_data['description'],
                                'post_title'   => htmlspecialchars( $_image_data['title'] ),
                            )
                        );
                    }
                }
            } catch ( MainWP_Exception $e ) {
                // ok!
            }
        }

        if ( ! $featured_image_exist ) {
            delete_post_meta( $new_post_id, '_thumbnail_id' );
        }
    }
}