<?php
/**
 * The REST API-specific functionality of the plugin.
 *
 * @link       https://disqus.com
 * @since      3.0
 *
 * @package    Disqus
 * @subpackage Disqus/rest-api
 */

/**
 * Defines the REST API endpoints for the plugin
 *
 * @package    Disqus
 * @subpackage Disqus/rest-api
 * @author     Ryan Valentin <ryan@disqus.com>
 */
class Disqus_Rest_Api {

    const REST_NAMESPACE = 'disqus/v1';

    const DISQUS_API_BASE = 'https://disqus.com/api/3.0/';

    /**
     * Instance of the Disqus API service.
     *
     * @since    3.0
     * @access   private
     * @var      Disqus_Api_Service    $api_service    Instance of the Disqus API service.
     */
    private $api_service;

    /**
     * The current version of the plugin.
     *
     * @since    3.0
     * @access   protected
     * @var      string    $version    The current version of the plugin.
     */
    protected $version;

    /**
     * Initialize the class and set its properties.
     *
     * @since    3.0
     * @param    Disqus_Api_Service $api_service    Instance of the Disqus API service.
     * @param    string             $version        The version of this plugin.
     */
    public function __construct( $api_service, $version ) {
        $this->api_service = $api_service;
        $this->version = $version;
    }

    /**
     * When added as a filter, allows customizing the response. We use this to stop WordPress from
     * JSON-encoding the webhook verification response.
     * See: https://github.com/WordPress/WordPress/blob/27aa0664df927f610e42f9a694eb452cee49e245/wp-includes/rest-api/class-wp-rest-server.php#L376
     *
     * @since     3.0
     * @param     boolean          $served     Whether this filter has already been applied.
     * @param     WP_HTTP_Response $result     Result to send to the client. Usually a WP_REST_Response.
     * @param     WP_REST_Request  $request    Request used to generate the response.
     * @param     WP_REST_Server   $server     Server instance.
     * @return    boolean                      Whether we've handled the response or not.
     */
    public function filter_rest_pre_serve_request( $served, $result, $request, $server ) {
        if ( '/' . Disqus_Rest_Api::REST_NAMESPACE . '/sync/webhook' === $request->get_route() ) {
            // The sync/webhook endpoint should never return anything except the challenge in
            // plain text when prompted.
            header( 'Content-Type: text/plain; charset=UTF-8' );
            $json_data = $request->get_json_params();
            echo 'verify' === $json_data['verb'] ? $json_data['challenge'] : '';
            $served = true;
        }

        return $served;
    }

    /**
     * Callback to ensure user has manage_options permissions.
     *
     * @since     3.0
     * @param     WP_REST_Request $request    The request object.
     * @return    boolean|WP_Error            Whether the user has permission to the admin REST API.
     */
    public function rest_admin_only_permission_callback( WP_REST_Request $request ) {
        // Regular cookie-based authentication.
        if ( current_user_can( 'manage_options' ) ) {
            return true;
        }

        // Shared secret authentication.
        $hub_signature = $request->get_header( 'X-Hub-Signature' );
        $sync_token = get_option( 'disqus_sync_token' );
        $body = $request->get_body();

        if ( $hub_signature &&
             $sync_token &&
             $body &&
            ( 'sha512=' . hash_hmac( 'sha512', $body, $sync_token ) ) === $hub_signature ) {
            return true;
        }

        return $this->rest_get_error( 'You must be logged in and have admin permissions for this resource.', 401 );
    }

    /**
     * Registers Disqus plugin WordPress REST API endpoints.
     *
     * @since    3.0
     */
    public function register_endpoints() {
        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'sync/webhook', array(
            'methods' => 'POST',
            'callback' => array( $this, 'rest_sync_webhook' ),
            'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
        ) );

        // Alias route for `sync/webhook` to get around plugins/services disabling "abused" routes.
        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'sync/comment', array(
            'methods' => 'POST',
            'callback' => array( $this, 'rest_sync_webhook' ),
            'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
        ) );

        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'settings', array(
            array(
                'methods' => array( 'GET', 'POST' ),
                'callback' => array( $this, 'rest_settings' ),
                'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
            ),
            'schema' => array( $this, 'dsq_get_settings_schema' ),
        ) );

        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'sync/status', array(
            array(
                'methods' => 'GET',
                'callback' => array( $this, 'rest_sync_status' ),
                'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
            ),
        ) );

        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'sync/enable', array(
            array(
                'methods' => 'POST',
                'callback' => array( $this, 'rest_sync_enable' ),
                'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
            ),
        ) );

        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'sync/disable', array(
            array(
                'methods' => 'POST',
                'callback' => array( $this, 'rest_sync_disable' ),
                'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
            ),
        ) );

        register_rest_route( Disqus_Rest_Api::REST_NAMESPACE, 'export/post', array(
            array(
                'methods' => 'POST',
                'callback' => array( $this, 'rest_export_post' ),
                'permission_callback' => array( $this, 'rest_admin_only_permission_callback' ),
            ),
        ) );
    }

    /**
     * Utility function to format REST API responses.
     *
     * @since    3.0
     * @access   private
     * @param    array $data         The request data to be returned.
     * @return   WP_REST_Response    The API response object.
     */
    private function rest_get_response( array $data ) {
        return new WP_REST_Response( array(
            'code' => 'OK',
            'message' => 'Request completed successfully',
            'data' => $data,
        ), 200 );
    }

    /**
     * Utility function to format REST API errors, and to optionally log them.
     *
     * @since    3.0
     * @access   private
     * @param    string $message        The error message to be returned.
     * @param    int    $status_code    The http status code of the error.
     * @return   WP_Error               The API error object.
     */
    private function rest_get_error( $message, $status_code = 500 ) {
        return new WP_Error(
            $status_code,
            $message,
            array(
                'status' => $status_code,
            )
        );
    }

    /**
     * Endpoint callback for sync/webhook. Takes a Disqus comment and saves
     * it to the local comments database.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_sync_webhook( WP_REST_Request $request ) {
        $json_data = $request->get_json_params();

        if ( ! isset( $json_data['verb'] ) ) {
            return new WP_Error( 400, 'Missing required property: verb.' );
        }

        // Check for and respond to verification request before anything else.
        if ( 'verify' === $json_data['verb'] ) {
            // The X-Hub-Signature header was already validated, so we only need to return the challenge.
            $this->log_sync_message( 'Syncing established with Disqus servers' );
            return new WP_REST_Response( $json_data['challenge'], 200 );
        }

        // Don't attempt to sync anything that isn't a comment.
        if ( 'post' !== $json_data['object_type'] ) {
            return new WP_REST_Response( '', 204 );
        }

        try {
            switch ( $json_data['verb'] ) {
                case 'force_sync':
                    $comment_id = $this->create_or_update_comment_from_post( $json_data['transformed_data'] );
                    $this->log_sync_message( 'Manually synced comment "' . $json_data['transformed_data']['id'] . '" from Disqus' );
                    return new WP_REST_Response( (string) $comment_id, 200 );
                case 'create':
                    $new_comment_id = $this->create_or_update_comment_from_post( $json_data['transformed_data'] );
                    $this->log_sync_message( 'Synced new comment "' . $json_data['transformed_data']['id'] . '" from Disqus' );
                    return new WP_REST_Response( (string) $new_comment_id, 201 );
                case 'update':
                    $updated_comment_id = $this->create_or_update_comment_from_post( $json_data['transformed_data'] );
                    $this->log_sync_message( 'Updated synced comment "' . $json_data['transformed_data']['id'] . '" from Disqus' );
                    return new WP_REST_Response( (string) $updated_comment_id, 200 );
                default:
                    return new WP_REST_Response( '', 204 );
            }
        } catch ( Exception $e ) {
            $this->log_sync_message( 'Error occurred during sync request from Disqus' );
            return new WP_Error( 500, (string) $e );
        }
    }

    /**
     * Endpoint callback for admin settings.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_settings( WP_REST_Request $request ) {
        $should_update = 'POST' === $request->get_method();
        $new_settings = $should_update ? $this->get_request_data( $request ) : null;

        // Validate sync token if set.
        if ( $should_update && isset( $new_settings['disqus_sync_token'] ) ) {
            if ( strlen( $new_settings['disqus_sync_token'] ) < 32 ) {
                return $this->rest_get_error( 'The site secret key should be at least 32 characters.' );
            }
        }

        $updated_settings = $this->get_or_update_settings( $new_settings );

        return $this->rest_get_response( $updated_settings );
    }

    /**
     * Endpoint callback for fetching automatic syncing status.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_sync_status( WP_REST_Request $request ) {
        try {
            $status = $this->get_sync_status();

            return $this->rest_get_response( $status );
        } catch ( Exception $e ) {
            return $this->rest_get_error( 'There was an error fetching sync status. Please go to Disqus.com and check that you are logged into a Disqus account with moderator privileges on this site, and then reload this page. For further troubleshooting, here is a 150 character error message preview: ' . substr( $e, 0, 150 ) );
        }
    }

    /**
     * Endpoint callback for enabling automatic syncing.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_sync_enable( WP_REST_Request $request ) {
        try {
            $status = $this->enable_sync();

            return $this->rest_get_response( $status );
        } catch ( Exception $e ) {
            return $this->rest_get_error( 'There was an error attempting to enable automatic syncing. Please go to Disqus.com and check that you are logged into a Disqus account with moderator privileges on this site, and then reload this page. For further troubleshooting, here is a 150 character error message preview: ' . substr( $e, 0, 150 ) );
        }
    }

    /**
     * Endpoint callback for disabling automatic syncing.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_sync_disable( WP_REST_Request $request ) {
        try {
            $status = $this->disable_sync();

            return $this->rest_get_response( $status );
        } catch ( Exception $e ) {
            return $this->rest_get_error( 'There was an error attempting to disable automatic syncing. Please go to Disqus.com and check that you are logged into a Disqus account with moderator privileges on this site, and then reload this page. For further troubleshooting, here is a 150 character error message preview: ' . substr( $e, 0, 150 ) );
        }
    }

    /**
     * Endpoint callback for exporting comments from a single WordPress post to Disqus.
     *
     * @since    3.0
     * @param    WP_REST_Request $request     The request object.
     * @return   WP_REST_Response|WP_Error    The API response object.
     */
    public function rest_export_post( WP_REST_Request $request ) {
        $json_data = $request->get_json_params();
        $postId = $json_data['postId'];

        if ( ! isset( $json_data['postId'] ) ) {
            return $this->rest_get_error( 'Missing required property: postId.', 400 );
        }

        $post = get_post( $postId );

        // First get comments that are approved, and sort them by smaller IDs first, so that the parent/child dependency
        // of replies are maintained.
        $comments = get_comments( array(
            'post_id' => $postId,
            'status' => 'approve',
            'orderby' => 'comment_ID',
            'order' => 'ASC',
        ) );

        // Filter out pingbacks/trackings and comments that have been created by Disqus via syncing.
        $filtered_comments = array_filter( $comments, array( $this, 'is_comment_exportable' ) );

        $response_data = array(
            'comments' => $filtered_comments,
        );

        if ( ! empty( $filtered_comments ) ) {
            // Generate a WXR (XML) file that Disqus will be able to read.
            $wxr = $this->generate_export_wxr( $post, $filtered_comments );
            $filename = (string) $post->ID . '.wxr';
            $response_data['wxr'] = array(
                'filename' => $filename,
                'xmlContent' => $wxr,
            );
        }

        return $this->rest_get_response( $response_data );
    }

    /**
     * Returns the schema for the Disqus admin settings REST endpoint.
     *
     * @since     3.0
     * @return    array    The REST schema.
     */
    public function dsq_get_settings_schema() {
        return array(
            // This tells the spec of JSON Schema we are using which is draft 4.
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            // The title property marks the identity of the resource.
            'title' => 'settings',
            'type' => 'object',
            // In JSON Schema you can specify object properties in the properties attribute.
            'properties' => array(
                'disqus_forum_url' => array(
                    'description' => 'Your site\'s unique identifier',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_sso_enabled' => array(
                    'description' => 'This will enable Single Sign-on for this site, if already enabled for your Disqus organization.',
                    'type' => 'boolean',
                    'readonly' => false,
                ),
                'disqus_public_key' => array(
                    'description' => 'The public key of your application.',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_secret_key' => array(
                    'description' => 'The secret key of your application.',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_admin_access_token' => array(
                    'description' => 'The primary admin\'s access token for your application.',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_sso_button' => array(
                    'description' => 'A link to a .png, .gif, or .jpg image to show as a button in Disqus.',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_sync_token' => array(
                    'description' => 'The shared secret token for data sync between Disqus and the plugin.',
                    'type' => 'string',
                    'readonly' => false,
                ),
                'disqus_installed' => array(
                    'description' => 'The shared secret token for data sync between Disqus and the plugin.',
                    'type' => 'boolean',
                    'readonly' => true,
                ),
                'disqus_render_js' => array(
                    'description' => 'When true, the Disqus embed javascript is output directly into markup rather than being enqueued in a separate file.',
                    'type' => 'boolean',
                    'readonly' => false,
                ),
            ),
        );
    }

    /**
     * Checks a comment state to determine if it's valid for exporting into Disqus.
     *
     * @since     3.0.11
     * @param     array $comment    The WordPress comment instance.
     * @return    boolean           Whether the comment is valid for exporting into Disqus.
     */
    private function is_comment_exportable( $comment ) {
        $is_comment = ( empty( $comment->comment_type ) || 'comment' === $comment->comment_type );
        $is_disqus_comment = strpos( $comment->comment_agent, 'Disqus' ) !== false;
        return $is_comment && ! $is_disqus_comment;
    }

    /**
     * Parses and returns body content for either form-url-encoded or json data.
     *
     * @since    3.0
     * @param    WP_REST_Request $request    The request object.
     * @return   array                       Array of parsed request data.
     */
    private function get_request_data( WP_REST_Request $request ) {
        $content_type = $request->get_content_type();
        if ( $content_type && $content_type['value'] ) {
            $content_type_val = $content_type['value'];
        } else {
            $content_type_val = null;
        }

        switch ( $content_type_val ) {
            case 'application/json':
                return $request->get_json_params();
            default:
                return $request->get_body_params();
        }
    }

    /**
     * Fetches all available plugin options and updates any values if passed, and returns updated array.
     *
     * @since     3.0
     * @param     array $new_settings    Any options to be updated.
     * @access    private
     * @return    array    The current settings array.
     */
    private function get_or_update_settings( $new_settings = null ) {
        $settings = array();
        $schema = $this->dsq_get_settings_schema();
        $should_update = is_array( $new_settings );

        // Loops through properties in our schema to check the value and update if needed.
        foreach ( $schema['properties'] as $key => $schema_value ) {
            $should_update_param = $should_update && isset( $new_settings[ $key ] ) && false === $schema_value['readonly'];
            if ( $should_update_param ) {
                update_option( $key, $new_settings[ $key ] );
            }
            $settings[ $key ] = get_option( $key, null );

            // Escape only values that have been set, otherwise esc_attr() will change null to an empty string.
            if ( null !== $settings[ $key ] ) {
                $settings[ $key ] = esc_attr( $settings[ $key ] );
            }
        }

        // Add additional non-database options here.
        $settings['disqus_installed'] = trim( $settings['disqus_forum_url'] ) !== '';

        return $settings;
    }

    /**
     * Determines if subscription information matches information about this WordPress site.
     *
     * @since    3.0
     * @param    object $subscription    The Disqus webhook subscription array.
     * @return   boolean                 Whether the subscription information belongs to this WordPress site.
     */
    private function validate_subscription( $subscription ) {
        return rest_url( Disqus_Rest_Api::REST_NAMESPACE . '/sync/webhook' ) === $subscription->url;
    }

    /**
     * Fetches and returns the syncing status from the Disqus servers.
     *
     * @since    3.0
     * @return   array        The syncing status array.
     * @throws   Exception    An exception if the Disqus API doesn't return with code 0 (status: 200).
     */
    private function get_sync_status() {
        $is_subscribed = false;
        $is_enabled = false;
        $current_subscription = null;
        $requires_update = false;

        if ( get_option( 'disqus_secret_key' ) && get_option( 'disqus_admin_access_token' ) ) {
            $api_data = $this->api_service->api_get( 'forums/webhooks/list', array(
                'forum' => get_option( 'disqus_forum_url' ),
            ));

            if ( 0 === $api_data->code ) {
                // Loop through each subscription, looking for the first match.
                foreach ( $api_data->response as $subscription ) {
                    if ( $this->validate_subscription( $subscription ) ) {
                        $current_subscription = $subscription;
                        $is_enabled = $current_subscription->enableSending;
                        $is_subscribed = true;
                        $requires_update = get_option( 'disqus_sync_token' ) !== $current_subscription->secret;
                        break;
                    }
                }
            } else {
                throw new Exception( $api_data->response );
            }
        }

        return array(
            'subscribed' => $is_subscribed,
            'enabled' => $is_enabled,
            'requires_update' => $requires_update,
            'subscription' => $current_subscription,
            'last_message' => get_option( 'disqus_last_sync_message', '' ),
        );
    }

    /**
     * Enables automatic syncing from Disqus if disabled.
     *
     * @since    3.0
     * @return   array    The syncing status array.
     * @throws   Exception    An exception if the Disqus API doesn't return with code 0 (status: 200).
     */
    private function enable_sync() {
        $sync_status = $this->get_sync_status();
        $subscription = $sync_status['subscription'];
        $endpoint = null;
        $params = array(
            'url' => rest_url( Disqus_Rest_Api::REST_NAMESPACE . '/sync/webhook' ),
            'secret' => get_option( 'disqus_sync_token' ),
            'enableSending' => '1',
        );

        if ( ! $sync_status['subscribed'] ) {
            $endpoint = 'forums/webhooks/create';
            $params['forum'] = get_option( 'disqus_forum_url' );
        } elseif ( ! $sync_status['enabled'] || $sync_status['requires_update'] ) {
            $endpoint = 'forums/webhooks/update';
            $params['subscription'] = $subscription->id;
        }

        if ( null !== $endpoint ) {
            $api_data = $this->api_service->api_post( $endpoint, $params );

            if ( 0 === $api_data->code ) {
                $sync_status = array(
                    'subscribed' => true,
                    'enabled' => true,
                    'requires_update' => false,
                    'subscription' => $api_data->response,
                    'last_message' => $sync_status['last_message'],
                );
            } else {
                $this->log_sync_message( 'Error enabling syncing: ' . $api_data->response );
                throw new Exception( $api_data->response );
            }
        }

        return $sync_status;
    }

    /**
     * Disables automatic syncing from Disqus if enabled.
     *
     * @since    3.0
     * @return   array    The syncing status array.
     * @throws   Exception    An exception if the Disqus API doesn't return with code 0 (status: 200).
     */
    private function disable_sync() {
        $sync_status = $this->get_sync_status();
        $subscription = $sync_status['subscription'];

        if ( $sync_status['enabled'] ) {
            $params = array(
                'subscription' => $subscription->id,
                'enableSending' => '0',
            );
            $api_data = $this->api_service->api_post( 'forums/webhooks/update', $params );

            if ( 0 === $api_data->code ) {
                $sync_status = array(
                    'subscribed' => true,
                    'enabled' => false,
                    'requires_update' => false,
                    'subscription' => $api_data->response,
                    'last_message' => $sync_status['last_message'],
                );
            } else {
                throw new Exception( $api_data->response );
            }
        }

        return $sync_status;
    }

    /**
     * Queries the WordPress database for existing comment by dsq_post_id. Creates or updates if comment found
     * in the WordPress database given a Disqus post.
     *
     * @since    3.0.17
     * @param    array $post    The Disqus post object.
     * @return   int            The created or updated comment ID.
     * @throws   Exception      An exception if comment can't be saved from post data.
     */
    private function create_or_update_comment_from_post( $post ) {
        $this->validate_disqus_post_data( $post );

        // Check for existing comment.
        $comment_query = new WP_Comment_Query( array(
            'meta_key' => 'dsq_post_id',
            'meta_value' => $post['id'],
            'number' => 1,
        ) );

        $comments = $comment_query->comments;
        if ( ! empty( $comments ) ) {
            return $this->update_comment_from_post( $post, $comments );
        }

        return $this->create_comment_from_post( $post );
    }

    /**
     * Creates a comment in the WordPress database given a Disqus post.
     *
     * @since    3.0
     * @param    array $post    The Disqus post object.
     * @return   int            The newly created comment ID.
     * @throws   Exception      An exception if comment can't be saved from post data.
     */
    private function create_comment_from_post( $post ) {
        $comment_data = $this->comment_data_from_post( $post );

        $new_comment_id = wp_insert_comment( $comment_data );

        return $new_comment_id;
    }

    /**
     * Updates a comment in the WordPress database given a Disqus post.
     *
     * @since    3.0
     * @param    array $post        The Disqus post object.
     * @param    array $comments    The comments found matching the dsq_post_id.
     * @return   int                The newly created comment ID.
     * @throws   Exception          An exception if comment can't be saved from post data.
     */
    private function update_comment_from_post( $post, $comments ) {
        foreach ( $comments as $comment ) {
            $updated_comment_id = $comment->comment_ID;
        }

        $comment_data = $this->comment_data_from_post( $post );
        $comment_data['comment_ID'] = $updated_comment_id;

        // Remove non-updating fields.
        unset( $comment_data['comment_meta'] );
        unset( $comment_data['comment_agent'] );
        unset( $comment_data['comment_type'] );
        unset( $comment_data['comment_date_gmt'] );
        unset( $comment_data['comment_post_ID'] );

        $updated = wp_update_comment( $comment_data );

        return 1 === $updated ? $updated_comment_id : 0;
    }

    /**
     * Creates a comment in the WordPress database given a Disqus post.
     *
     * @since    3.0
     * @param    array $post    The Disqus post object.
     * @throws   Exception      An exception if comment is invalid.
     */
    private function validate_disqus_post_data( $post ) {
        if ( ! $post || get_option( 'disqus_forum_url' ) !== $post['forum'] ) {
            throw new Exception( 'The comment\'s forum does not match the installed forum. Was "' . $post['forum'] . '", expected "' . get_option( 'disqus_forum_url' ) . '"' );
        }
    }

    /**
     * Checks the state of a Disqus post (comment) and translates to the WordPress comment_approved format.
     *
     * @since    3.0
     * @param    array $post    The Disqus post object.
     * @return   array          The translated comment data to be inserted/updated.
     * @throws   Exception      An exception if comment can't be saved from post data.
     */
    private function comment_data_from_post( $post ) {
        $thread = array_key_exists( 'threadData', $post ) ? $post['threadData'] : $post['thread'];
        $author = $post['author'];

        $wp_post_id = null;

        // Look up posts with the Disqus thread ID meta field.
        $post_query = new WP_Query( array(
            'meta_key' => 'dsq_thread_id',
            'meta_value' => $thread['id'],
            'ignore_sticky_posts' => true,
        ) );

        if ( $post_query->have_posts() ) {
            $wp_post_id = $post_query->post->ID;
            wp_reset_postdata();
        }

        // If that doesn't exist, get the  and update the matching post metadata.
        if ( null === $wp_post_id || false === $wp_post_id ) {
            $identifiers = $thread['identifiers'];
            $first_identifier = count( $identifiers ) > 0 ? $identifiers[0] : null;

            if ( null !== $first_identifier ) {
                $ident_parts = explode( ' ', $first_identifier, 2 );
                $wp_post_id = reset( $ident_parts );
            }

            // Keep the post's thread ID meta up to date.
            update_post_meta( $wp_post_id, 'dsq_thread_id', $thread['id'] );
        }

        // Find the parent comment, if any.
        // To simplify our syncing process and prevent syncing errors,
        // still sync the comment even if we don't have its parent comment synced.
        $parent = 0;
        if ( null !== $post['parent'] ) {
            $parent_comment_query = new WP_Comment_Query( array(
                'meta_key' => 'dsq_post_id',
                'meta_value' => (string) $post['parent'],
                'number' => 1,
            ) );
            $parent_comments = $parent_comment_query->comments;

            if ( $parent_comments ) {
                $parent = $parent_comments[0]->comment_ID;
            }
        }

        // Email is a special permission for Disqus API applications and won't be present
        // if the user has not set the permission for their API application.
        $author_email = null;
        if ( isset( $author['email'] ) ) {
            $author_email = $author['email'];
        } elseif ( $author['isAnonymous'] ) {
            $author_email = 'anonymized-' . md5( $author['name'] ) . '@disqus.com';
        } else {
            $author_email = 'user-' . $author['id'] . '@disqus.com';
        }

        // Translate the comment approval state.
        $comment_approved = 1;
        if ( $post['isApproved'] && ! $post['isDeleted'] ) {
            $comment_approved = 1;
        } elseif ( $post['isDeleted'] ) {
            $comment_approved = 0; // Deleted is not a state in WordPress, so we'll keep them in pending.
        } elseif ( ! $post['isDeleted'] && ! $post['isSpam'] && ! $post['isApproved'] ) {
            $comment_approved = 0;
        } elseif ( $post['isSpam'] && ! $post['isApproved'] ) {
            $comment_approved = 'spam';
        }

        return array(
            'comment_post_ID' => (int) $wp_post_id,
            'comment_author' => $author['name'],
            'comment_author_email' => $author_email,
            'comment_author_IP' => $post['ipAddress'],
            'comment_author_url' => isset( $author['url'] ) ? $author['url'] : '',
            'comment_content' => $post['raw_message'],
            'comment_date' => $post['createdAt'],
            'comment_date_gmt' => $post['createdAt'],
            'comment_type' => '', // Leave blank for a regular comment.
            'comment_parent' => $parent,
            'comment_agent' => 'Disqus Sync Host',
            'comment_approved' => $comment_approved,
            'comment_meta' => array(
                'dsq_post_id' => $post['id'],
            ),
        );
    }

    /**
     * Stores the last sync log message with date/time appended.
     *
     * @since     3.0
     * @access    private
     * @param     string $message    The base message to store.
     */
    private function log_sync_message( $message ) {
        update_option( 'disqus_last_sync_message', $message . ': ' . date( 'Y-m-d g:i a', time() ) );
    }

    /**
     * Outputs a list of comments to a WXR file for uploading to the Disqus importer.
     *
     * @since     3.0
     * @access    private
     * @param     WP_Post $post        The post details the comments belong to.
     * @param     array   $comments    The base message to store.
     * @return    string               The WXR document as a string.
     */
    private function generate_export_wxr( $post, $comments ) {

        $post_author = get_userdata( $post->post_author );

        $xml = new DOMDocument( '1.0', get_bloginfo( 'charset' ) );

        $rss = $xml->createElement( 'rss' );
        $rss->setAttribute( 'version', '2.0' );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:excerpt',
            'http://wordpress.org/export/1.0/excerpt/'
        );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:content',
            'http://purl.org/rss/1.0/modules/content/'
        );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:dsq',
            'https://disqus.com/'
        );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:wfw',
            'http://wellformedweb.org/CommentAPI/'
        );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:dc',
            'http://purl.org/dc/elements/1.1/'
        );
        $rss->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:wp',
            'http://wordpress.org/export/1.0/'
        );

        $channel = $xml->createElement( 'channel' );
        $channel->appendChild( $xml->createElement( 'title', get_bloginfo_rss( 'name' ) ) );
        $channel->appendChild( $xml->createElement( 'link', get_bloginfo_rss( 'url' ) ) );
        $channel->appendChild(
            $xml->createElement(
                'pubDate',
                mysql2date( 'D, d M Y H:i:s +0000', get_lastpostmodified( 'GMT' ), false )
            )
        );
        $channel->appendChild(
            $xml->createElement(
                'generator',
                'WordPress ' . get_bloginfo_rss( 'version' ) . '; Disqus ' . $this->version
            )
        );

        // Generate the item (the post).
        $item = $xml->createElement( 'item' );
        $item->appendChild(
            $xml->createElement( 'title', apply_filters( 'the_title_rss', $post->post_title ) )
        );
        $item->appendChild(
            $xml->createElement(
                'link',
                esc_url( apply_filters( 'the_permalink_rss', get_permalink( $post->ID ) ) )
            )
        );
        $item->appendChild(
            $xml->createElement(
                'pubDate',
                mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true, $post ), false )
            )
        );

        $author_name_cdata = $xml->createCDATASection( $post_author->display_name );
        $author_name_element = $xml->createElement( 'dc:creator' );
        $author_name_element->appendChild( $author_name_cdata );
        $item->appendChild( $author_name_element );

        $guid = $xml->createElement( 'guid', $post->guid );
        $guid->setAttribute( 'isPermalink', 'false' );
        $item->appendChild( $guid );

        $post_content_cdata = $xml->createCDATASection( apply_filters( 'the_content_export', $post->post_content ) );
        $post_content_element = $xml->createElement( 'content:encoded' );
        $post_content_element->appendChild( $post_content_cdata );
        $item->appendChild( $post_content_element );

        $identifier_cdata = $xml->createCDATASection( $post->ID . ' ' . $post->guid );
        $identifier_element = $xml->createElement( 'dsq:thread_identifier' );
        $identifier_element->appendChild( $identifier_cdata );
        $item->appendChild( $identifier_element );

        $item->appendChild(
            $xml->createElement(
                'wp:post_id',
                $post->ID
            )
        );

        $item->appendChild(
            $xml->createElement(
                'wp:post_date_gmt',
                $post->post_date_gmt
            )
        );

        $item->appendChild(
            $xml->createElement(
                'wp:comment_status',
                $post->comment_status
            )
        );

        foreach ( $comments as $c ) {

            $wpcomment = $xml->createElement( 'wp:comment' );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_id',
                    $c->comment_ID
                )
            );

            $comment_author_name_cdata = $xml->createCDATASection( $c->comment_author );
            $comment_author_name_element = $xml->createElement( 'wp:comment_author' );
            $comment_author_name_element->appendChild( $comment_author_name_cdata );
            $wpcomment->appendChild( $comment_author_name_element );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_author_email',
                    $c->comment_author_email
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_author_url',
                    $c->comment_author_url
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_author_IP',
                    $c->comment_author_IP
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_date',
                    $c->comment_date
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_date_gmt',
                    $c->comment_date_gmt
                )
            );

            $comment_content_cdata = $xml->createCDATASection( $c->comment_content );
            $comment_content_element = $xml->createElement( 'wp:comment_content' );
            $comment_content_element->appendChild( $comment_content_cdata );
            $wpcomment->appendChild( $comment_content_element );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_approved',
                    $c->comment_approved
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_type',
                    $c->comment_type
                )
            );

            $wpcomment->appendChild(
                $xml->createElement(
                    'wp:comment_parent',
                    $c->comment_parent
                )
            );

            $item->appendChild( $wpcomment );
        }

        // Append the post item to the channel.
        $channel->appendChild( $item );

        // Append the root channel to the RSS element.
        $rss->appendChild( $channel );

        // Finally append the root RSS element to the XML document.
        $xml->appendChild( $rss );

        $wxr = $xml->saveXML();

        return $wxr;
    }
}
