Source: classes/ExternalConnections/WordPressExternalConnection.php

<?php
/**
 * WP external connection functionality
 *
 * @package  distributor
 */

namespace Distributor\ExternalConnections;

use Distributor\DistributorPost;
use \Distributor\ExternalConnection as ExternalConnection;
use \Distributor\Utils;

/**
 * Class handling WP external connections
 */
class WordPressExternalConnection extends ExternalConnection {

	/**
	 * Connection slug
	 *
	 * @var string
	 */
	public static $slug = 'wp';

	/**
	 * Connection pretty label
	 *
	 * This is to represent the authentication method,
	 * not the connection type. This value was previously
	 * "WordPress REST API".
	 *
	 * @since 1.4.0 Label as authentication method, not connection type
	 *
	 * @var string
	 */
	public static $label = 'Username / Password';

	/**
	 * Auth handler to use
	 *
	 * @var string
	 */
	public static $auth_handler_class = '\Distributor\Authentications\WordPressBasicAuth';

	/**
	 * REST API namespace
	 *
	 * @var string
	 */
	public static $namespace = 'wp/v2';

	/**
	 * Remote request timeout
	 *
	 * @var integer
	 */
	public static $timeout = 5;

	/**
	 * Default post type to pull.
	 *
	 * @var string
	 */
	public $pull_post_type;

	/**
	 * Default post types supported.
	 *
	 * @var string
	 */
	public $pull_post_types;

	/**
	 * This is a utility function for parsing annoying API link headers returned by the types endpoint
	 *
	 * @param  array $type Types array.
	 * @since  0.8
	 * @return string|bool
	 */
	private function parse_type_items_link( $type ) {
		try {
			if ( isset( $type['_links']['wp:items'][0]['href'] ) ) {
				$link = $type['_links']['wp:items'][0]['href'];
				return $link;
			}
		} catch ( \Exception $e ) {
			// Bummer
		}

		try {
			if ( isset( $type['_links']['https://api.w.org/items'][0]['href'] ) ) {
				$link = $type['_links']['https://api.w.org/items'][0]['href'];
				return $link;
			}
		} catch ( \Exception $e ) {
			// Even bigger bummer
		}

		return false;
	}

	/**
	 * Remotely get posts
	 *
	 * @param  array $args Remote get args.
	 * @since  0.8
	 * @return array|\WP_Post|\WP_Error
	 */
	public function remote_get( $args = array() ) {
		$id = ( empty( $args['id'] ) ) ? false : $args['id'];

		$query_args = array();

		$post_type = ( empty( $args['post_type'] ) ) ? 'post' : $args['post_type'];

		if ( empty( $id ) ) {
			$query_args['post_status'] = ( empty( $args['post_status'] ) ) ? 'any' : $args['post_status'];
			$posts_per_page            = ( empty( $args['posts_per_page'] ) ) ? get_option( 'posts_per_page' ) : $args['posts_per_page'];
			$query_args['page']        = ( empty( $args['paged'] ) ) ? 1 : $args['paged'];

			if ( isset( $args['post__in'] ) ) {
				// If post__in is empty, we can just stop right here
				if ( empty( $args['post__in'] ) ) {
					/**
					 * Filter the remote_get request.
					 *
					 * @since 1.0
					 * @hook dt_remote_get
					 *
					 * @param {array} $args  The arguments originally passed to `remote_get`.
					 * @param {object} $this The authentication class.
					 *
					 * @return {array} The arguments originally passed to `remote_get`.
					 */
					return apply_filters(
						'dt_remote_get',
						[
							'items'       => array(),
							'total_items' => 0,
						],
						$args,
						$this
					);
				}

				$query_args['include'] = $args['post__in'];
			} elseif ( isset( $args['post__not_in'] ) ) {
				// phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
				$query_args['exclude'] = $args['post__not_in'];
			}

			if ( ! empty( $args['s'] ) ) {
				$query_args['search'] = $args['s'];
			}

			if ( ! empty( $args['orderby'] ) ) {
				if ( 'post__in' === $args['orderby'] ) {
					$query_args['orderby'] = 'include';
				} else {
					$query_args['orderby'] = strtolower( $args['orderby'] );
				}
			}

			if ( ! empty( $args['order'] ) ) {
				$query_args['order'] = strtolower( $args['order'] );
			}
		}

		// When running a query for the Pull screen, make a POST request instead
		if ( empty( $id ) ) {
			$query_args['post_type']      = isset( $post_type ) ? $post_type : 'post';
			$query_args['posts_per_page'] = isset( $posts_per_page ) ? $posts_per_page : 20;

			$posts_response = $this->remote_post(
				untrailingslashit( $this->base_url ) . '/' . self::$namespace . '/distributor/list-pull-content',
				$query_args
			);

			return $posts_response;
		}

		$query_args     = array(
			'include'   => absint( $id ),
			'post_type' => isset( $args['post_type'] ) ? $args['post_type'] : 'any',
		);
		$posts_response = $this->remote_post(
			untrailingslashit( $this->base_url ) . '/wp/v2/distributor/list-pull-content',
			$query_args
		);

		if ( is_wp_error( $posts_response ) ) {
			return $posts_response;
		}

		return $posts_response['items'][0];
	}

	/**
	 * Make a remote_post request.
	 *
	 * @param string $url Endpoint URL.
	 * @param array  $args Query arguments
	 * @return array|\WP_Error
	 */
	public function remote_post( $url = '', $args = array() ) {
		if ( ! $url ) {
			return new \WP_Error( 'endpoint-error', esc_html__( 'Endpoint URL must be set.', 'distributor' ) );
		}

		/**
		* Filter the remote_post query arguments
		*
		* @since 1.6.7
		* @hook dt_remote_post_query_args
		*
		* @param {array}  $args The request arguments.
		* @param {WordPressExternalConnection} $this The current connection object.
		*
		* @return {array} The query arguments.
		*/
		$body = apply_filters( 'dt_remote_post_query_args', $args, $this );

		// Add request parameter to specify Distributor request
		$body['distributor_request'] = '1';

		$request = wp_remote_post(
			$url,
			$this->auth_handler->format_post_args(
				array(
					/**
					 * Filter the timeout used when calling `remote_post`
					 *
					 * @since 1.6.7
					 * @hook dt_remote_post_timeout
					 *
					 * @param {int}   $timeout The timeout to use for the remote post. Default `45`.
					 * @param {array} $args    The request arguments.
					 *
					 * @return {int} The timeout to use for the remote_post call.
					 */
					'timeout' => apply_filters( 'dt_remote_post_timeout', 45, $args ),
					'body'    => $body,
				)
			)
		);

		if ( is_wp_error( $request ) ) {
			return $request;
		}

		$response_code = wp_remote_retrieve_response_code( $request );

		if ( 200 !== $response_code ) {
			if ( 404 === $response_code ) {
				return new \WP_Error( 'bad-endpoint', esc_html__( 'Could not connect to API endpoint.', 'distributor' ) );
			}

			$posts_body = json_decode( wp_remote_retrieve_body( $request ), true );

			$code    = empty( $posts_body['code'] ) ? 'endpoint-error' : esc_html( $posts_body['code'] );
			$message = empty( $posts_body['message'] ) ? esc_html__( 'API endpoint error.', 'distributor' ) : esc_html( $posts_body['message'] );

			return new \WP_Error( $code, $message );
		}

		$posts_body       = wp_remote_retrieve_body( $request );
		$response_headers = wp_remote_retrieve_headers( $request );

		if ( empty( $posts_body ) ) {
			return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) );
		}

		if (
			false === Utils\is_development_version()
			&& isset( $response_headers['x-distributor'] )
			&& (
				! isset( $response_headers['x-distributor-version'] )
				|| version_compare( $response_headers['x-distributor-version'], '2.0.0', '<' )
			)
		) {
			$version_error = new \WP_Error();
			$version_error->add(
				'old-distributor-version',
				esc_html__( 'Pulling content from external connections requires Distributor version 2.0.0 or later.', 'distributor' )
			);
			$version_error->add(
				'old-distributor-version',
				esc_html__( 'Please update Distributor on the site you are pulling content from.', 'distributor' )
			);
			return $version_error;
		}

		$posts           = json_decode( $posts_body, true );
		$formatted_posts = array();

		foreach ( $posts as $post ) {
			$post['full_connection'] = ! empty( $response_headers['X-Distributor'] );

			$formatted_posts[] = $this->to_wp_post( $post );
		}

		$total_posts = ! empty( $response_headers['X-WP-Total'] ) ? $response_headers['X-WP-Total'] : count( $formatted_posts );

		/**
		 * Filter the items returned when using `WordPressExternalConnection::remote_post`
		 *
		 * @since 1.6.7
		 * @hook dt_remote_post
		 *
		 * @param {array}                       $items The items returned from the POST request.
		 * @param {array}                       $args  The arguments used in the POST request.
		 * @param {WordPressExternalConnection} $this  The current connection object.
		 *
		 * @return {array} The items returned from a remote POST request.
		 */
		return apply_filters(
			'dt_remote_post',
			[
				'items'       => $formatted_posts,
				'total_items' => $total_posts,
			],
			$args,
			$this
		);
	}

	/**
	 * Pull items. Pass array of posts, each post should look like:
	 * [ 'remote_post_id' => POST ID TO GET, 'post_id' (optional) => POST ID TO MAP TO ]
	 *
	 * @param  array $items Posts to pull.
	 * @since  0.8
	 * @return array
	 */
	public function pull( $items ) {
		$created_posts = array();

		$remote_post_args = array(
			'include'   => array(),
			'post_type' => array(),
		);
		foreach ( $items as $item_array ) {
			$remote_post_args['include'][]   = $item_array['remote_post_id'];
			$remote_post_args['post_type'][] = $item_array['post_type'];
		}
		$remote_post_args['include']        = array_unique( $remote_post_args['include'] );
		$remote_post_args['post_type']      = array_unique( $remote_post_args['post_type'] );
		$remote_post_args['posts_per_page'] = count( $remote_post_args['include'] );

		// Get all remote posts in a single request.
		$remote_posts = $this->remote_post(
			untrailingslashit( $this->base_url ) . '/' . self::$namespace . '/distributor/list-pull-content',
			$remote_post_args
		);

		if ( is_wp_error( $remote_posts ) ) {
			return $remote_posts;
		}

		foreach ( $items as $item_array ) {
			$post = wp_list_filter( $remote_posts['items'], array( 'ID' => $item_array['remote_post_id'] ) );
			if ( empty( $post ) ) {
				$created_posts[] = new \WP_Error( 'no-post', esc_html__( 'No post found.', 'distributor' ) );
				continue;
			}
			$post = reset( $post );

			$post_props = get_object_vars( $post );
			$post_array = array();

			foreach ( $post_props as $key => $value ) {
				$post_array[ $key ] = $value;
			}

			$update = false;
			// Unset data from remote site.
			unset( $post_array['ID'] );
			unset( $post_array['post_parent'] );
			unset( $post_array['post_date'] );
			unset( $post_array['post_date_gmt'] );
			unset( $post_array['post_modified'] );
			unset( $post_array['post_modified_gmt'] );
			unset( $post_array['post_author'] );

			if ( ! empty( $item_array['post_id'] ) ) {
				$update           = true;
				$post_array['ID'] = $item_array['post_id'];
			}

			if ( ! empty( $item_array['post_status'] ) ) {
				$post_array['post_status'] = $item_array['post_status'];
			}

			/**
			 * Filter the arguments passed into wp_insert_post during a pull.
			 *
			 * @since 1.0
			 * @hook dt_pull_post_args
			 *
			 * @param  {array}              $post_array      The post data to be inserted.
			 * @param  {array}              $remote_post_id  The remote post ID.
			 * @param  {object}             $post            The request that got the post.
			 * @param  {ExternalConnection} $this            The Distributor connection pulling the post.
			 *
			 * @return {array} The post data to be inserted.
			 */
			$new_post_args = Utils\post_args_allow_list( apply_filters( 'dt_pull_post_args', $post_array, $item_array['remote_post_id'], $post, $this ) );
			if ( $update ) {
				$new_post = wp_update_post( wp_slash( $new_post_args ) );
			} else {
				$new_post = wp_insert_post( wp_slash( $new_post_args ) );
			}

			update_post_meta( $new_post, 'dt_original_post_id', (int) $item_array['remote_post_id'] );
			update_post_meta( $new_post, 'dt_original_source_id', (int) $this->id );
			update_post_meta( $new_post, 'dt_syndicate_time', time() );
			update_post_meta( $new_post, 'dt_original_post_url', esc_url_raw( $post_array['link'] ) );
			update_post_meta( $new_post, 'dt_original_site_name', sanitize_text_field( $post_array['original_site_name'] ) );
			update_post_meta( $new_post, 'dt_original_site_url', sanitize_text_field( $post_array['original_site_url'] ) );

			if ( ! empty( $post->post_parent ) ) {
				update_post_meta( $new_post, 'dt_original_post_parent', (int) $post->post_parent );
			}

			if ( empty( $post_array['full_connection'] ) ) {
				update_post_meta( $new_post, 'dt_full_connection', false );
			} else {
				update_post_meta( $new_post, 'dt_full_connection', true );
			}

			if ( isset( $post_array['meta'] ) && is_array( $post_array['meta'] ) ) {
				// Filter documented in includes/classes/InternalConnections/NetworkSiteConnection.php.
				if ( apply_filters( 'dt_pull_post_meta', true, $new_post, $post_array['meta'], $item_array['remote_post_id'], $post_array, $this ) ) {
					\Distributor\Utils\set_meta( $new_post, $post_array['meta'] );
				}
			}

			if ( ! empty( $post_array['terms'] ) ) {
				// Filter documented in includes/classes/InternalConnections/NetworkSiteConnection.php.
				if ( apply_filters( 'dt_pull_post_terms', true, $new_post, $post_array['terms'], $item_array['remote_post_id'], $post_array, $this ) ) {
					\Distributor\Utils\set_taxonomy_terms( $new_post, $post_array['terms'] );
				}
			}

			if ( ! empty( $post_array['media'] ) ) {

				// Filter documented in includes/classes/InternalConnections/NetworkSiteConnection.php.
				if ( apply_filters( 'dt_pull_post_media', true, $new_post, $post_array['media'], $item_array['remote_post_id'], $post_array, $this ) ) {
					\Distributor\Utils\set_media( $new_post, $post_array['media'] );
				}
			}

			// Action documented in includes/classes/InternalConnections/NetworkSiteConnection.php.
			do_action( 'dt_pull_post', $new_post, $this, $post_array );

			$created_posts[] = $new_post;
		}

		return $created_posts;
	}

	/**
	 * Push a post to an external connection
	 *
	 * @param  int|WP_Post $post Post or Post ID to push. Required.
	 * @param  array       $args Post args to push. Optional.
	 * @since  0.8
	 * @return array|\WP_Error
	 */
	public function push( $post, $args = array() ) {
		if ( empty( $post ) ) {
			return new \WP_Error( 'no-push-post-id', esc_html__( 'Post ID required to push.', 'distributor' ) );
		}
		$post = get_post( $post );
		if ( empty( $post ) ) {
			return new \WP_Error( 'invalid-push-post-id', esc_html__( 'Post does not exist.', 'distributor' ) );
		}


		$dt_post   = new DistributorPost( $post );
		$post_id   = $dt_post->post->ID;
		$post_type = $dt_post->get_post_type();
		$path      = self::$namespace;

		/*
		 * First let's get the actual route. We don't know the "plural" of our post type
		 */
		$types_path = untrailingslashit( $this->base_url ) . '/' . $path . '/types';

		$response = Utils\remote_http_request(
			$types_path,
			$this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) )
		);

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$body = wp_remote_retrieve_body( $response );

		if ( empty( $body ) ) {
			return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) );
		}

		$body_array = json_decode( $body, true );

		$type_url = $this->parse_type_items_link( $body_array[ $post_type ] );

		if ( empty( $type_url ) ) {
			return new \WP_Error( 'no-push-post-type', esc_html__( 'Could not determine remote post type endpoint.', 'distributor' ) );
		}

		$signature = \Distributor\Subscriptions\generate_signature();

		/*
		 * Now let's push
		 */
		$rest_args = array(
			'status'                         => ( ! empty( $args['post_status'] ) ) ? $args['post_status'] : 'publish',
			'distributor_signature'          => $signature,
			'distributor_original_source_id' => $this->id,
		);
		$post_body = $dt_post->to_rest( $rest_args );

		// Map to remote ID if a push has already happened
		if ( ! empty( $args['remote_post_id'] ) ) {
			$existing_post_url = untrailingslashit( $type_url ) . '/' . $args['remote_post_id'];

			$post_exists_response = Utils\remote_http_request( $existing_post_url, $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) );

			if ( ! is_wp_error( $post_exists_response ) ) {
				$post_exists_response_code = wp_remote_retrieve_response_code( $post_exists_response );

				if ( 200 === (int) $post_exists_response_code ) {
					$type_url = $existing_post_url;
				}
			}
		}

		$response = wp_remote_post(
			$type_url,
			$this->auth_handler->format_post_args(
				array(
					/**
					 * Filter the timeout used when calling `WordPressExternalConnection::push`.
					 *
					 * @since 1.0
					 * @hook dt_push_post_timeout
					 *
					 * @param {int} $timeout The timeout to use for the remote post. Default `5`.
					 * @param {object} $post The post object
					 *
					 * @return {int} The timeout to use for the remote post.
					 */
					'timeout' => apply_filters( 'dt_push_post_timeout', 45, $post ),
					/**
					 * Filter the arguments sent to the remote server during a push.
					 *
					 * @since 1.0
					 * @hook dt_push_post_args
					 * @tutorial snippets
					 *
					 * @param  {array}              $post_body  The request body to send.
					 * @param  {object}             $post       The WP_Post that is being pushed.
					 * @param  {array}              $args       Post args to push.
					 * @param  {ExternalConnection} $this       The distributor connection being pushed to.
					 *
					 * @return {array} The request body to send.
					 */
					'body'    => apply_filters( 'dt_push_post_args', $post_body, $post, $args, $this ),
				)
			)
		);

		// Action documented in includes/classes/InternalConnections/NetworkSiteConnection.php.
		do_action_deprecated(
			'dt_push_post',
			array( $response, $post_body, $type_url, $post_id, $args, $this ),
			'2.0.0',
			'dt_push_network_post|dt_push_external_post'
		);

		/**
		 * Fires the action after a post is pushed via Distributor before remote request validation.
		 *
		 * @since 2.0.0
		 * @hook  dt_push_external_post
		 *
		 * @param {array|WP_Error}              $response    The response from the remote request.
		 * @param {array}                       $post_body   The Post data formatted for the REST API endpoint.
		 * @param {string}                      $type_url    The Post type api endpoint.
		 * @param {int}                         $post_id     The Post id.
		 * @param {array}                       $args        The arguments passed into wp_insert_post.
		 * @param {WordPressExternalConnection} $this        The Distributor connection being pushed to.
		 */
		do_action( 'dt_push_external_post', $response, $post_body, $type_url, $post_id, $args, $this );

		if ( is_wp_error( $response ) ) {
			return $response;
		}

		$body = wp_remote_retrieve_body( $response );

		if ( empty( $body ) ) {
			return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) );
		}

		$body_array = json_decode( $body, true );

		if ( empty( $body_array['id'] ) ) {
			return new \WP_Error( 'no-push-post-remote-id', esc_html__( 'Could not determine remote post ID.', 'distributor' ) );
		}

		$response_headers = wp_remote_retrieve_headers( $response );

		if ( ! empty( $response_headers['X-Distributor'] ) ) {
			// We have Distributor on the other side
			\Distributor\Subscriptions\create_subscription( $post_id, $body_array['id'], untrailingslashit( $this->base_url ), $signature );
		}

		$remote_post = array(
			'id' => $body_array['id'],
		);

		if ( ! empty( $body_array['push-errors'] ) ) {
			$remote_post['push-errors'] = $body_array['push-errors'];
		}

		return $remote_post;
	}

	/**
	 * Get the available post types.
	 *
	 * @since 1.3
	 * @return array|\WP_Error
	 */
	public function get_post_types() {
		$path = self::$namespace;

		$types_path = untrailingslashit( $this->base_url ) . '/' . $path . '/types';

		$types_response = Utils\remote_http_request(
			$types_path,
			$this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) )
		);

		if ( is_wp_error( $types_response ) ) {
			return $types_response;
		}

		if ( 404 === wp_remote_retrieve_response_code( $types_response ) ) {
			return new \WP_Error( 'bad-endpoint', esc_html__( 'Could not connect to API endpoint.', 'distributor' ) );
		}

		$types_body = wp_remote_retrieve_body( $types_response );

		if ( empty( $types_body ) ) {
			return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) );
		}

		$types_body_array = json_decode( $types_body, true );

		return $types_body_array;
	}

	/**
	 * Check what we can do with a given external connection (push or pull)
	 *
	 * @since  0.8
	 * @return array
	 */
	public function check_connections() {
		$output = array(
			'errors'              => array(),
			'can_post'            => array(),
			'can_get'             => array(),
			'endpoint_suggestion' => false,
		);

		$remote_request_url = untrailingslashit( $this->base_url );
		if ( str_ends_with( $remote_request_url, '?rest_route=' ) ) {
			// It's a request to get the REST API index using plain permalinks and needs a trailing slash.
			$remote_request_url = trailingslashit( $remote_request_url );
		}

		$response = Utils\remote_http_request( $remote_request_url, $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) );

		$body = wp_remote_retrieve_body( $response );

		if ( is_wp_error( $response ) || empty( $body ) ) {
			$output['errors']['no_external_connection'] = 'no_external_connection';
			return $output;
		}

		$data = json_decode( $body, true );

		if ( empty( $data ) ) {
			$output['errors']['no_external_connection'] = 'no_external_connection';
		}

		$response_headers = wp_remote_retrieve_headers( $response );
		$link_headers     = (array) $response_headers['Link'];
		$correct_endpoint = false;

		foreach ( $link_headers as $link_header ) {
			if ( strpos( $link_header, 'rel="https://api.w.org/"' ) !== false ) {
				$correct_endpoint = preg_replace( '#.*<([^>]+)>.*#', '$1', $link_header );
			}
		}

		if ( ! empty( $correct_endpoint ) && untrailingslashit( $this->base_url ) !== untrailingslashit( $correct_endpoint ) ) {
			$output['errors']['no_external_connection'] = 'no_external_connection';
			$output['endpoint_suggestion']              = untrailingslashit( $correct_endpoint );
		}

		if ( empty( $data['routes'] ) && empty( $output['errors']['no_external_connection'] ) ) {
			$output['errors']['no_types'] = 'no_types';
		}

		if ( ! empty( $output['errors'] ) ) {
			return $output;
		}

		if ( empty( $response_headers['X-Distributor'] ) ) {
			$output['errors']['no_distributor'] = 'no_distributor';
		}

		$types_path = untrailingslashit( $this->base_url ) . '/' . self::$namespace . '/types';

		$types_response = Utils\remote_http_request( $types_path, $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) );

		$types_body = wp_remote_retrieve_body( $types_response );
		$types      = json_decode( $types_body, true );

		if ( is_wp_error( $types_response ) || 200 !== wp_remote_retrieve_response_code( $types_response ) || empty( $types_body ) || empty( $types ) ) {
			$output['errors']['no_types'] = 'no_types';
		} else {
			$can_get  = array();
			$can_post = array();

			$permission_url      = untrailingslashit( $this->base_url ) . '/' . self::$namespace . '/distributor/post-types-permissions';
			$permission_response = Utils\remote_http_request(
				$permission_url,
				$this->auth_handler->format_get_args(
					array(
						'timeout' => self::$timeout,
					)
				)
			);

			$permissions = json_decode( wp_remote_retrieve_body( $permission_response ) );

			$output['is_authenticated'] = isset( $permissions->is_authenticated ) ? $permissions->is_authenticated : 'na';

			if (
				is_wp_error( $permission_response )
				|| empty( $permissions )
				|| ! isset( $permissions->can_get )
				|| ! isset( $permissions->can_post )
			) {
				$output['errors']['no_permissions'] = 'no_permissions';
			} else {
				$can_get = array_values(
					array_filter(
						$permissions->can_get,
						[ $this, 'not_distributor_internal_post_type' ]
					)
				);

				$can_post = array_values(
					array_filter(
						$permissions->can_post,
						[ $this, 'not_distributor_internal_post_type' ]
					)
				);
			}

			$output['can_get']  = $can_get;
			$output['can_post'] = $can_post;
		}

		return $output;
	}


	/**
	 * Whether if the post type is not distributor internal post type.
	 *
	 * @param string $post_type Post type
	 *
	 * @return bool
	 */
	private function not_distributor_internal_post_type( $post_type ) {
		return 'dt_subscription' !== $post_type;
	}


	/**
	 * Convert array to WP_Post object suitable for insert/update.
	 *
	 * Some field names in the REST API response object do not match DB field names.
	 *
	 * @see    \WP_REST_Posts_Controller::prepare_item_for_database()
	 * @param  array $post Post as array.
	 * @since  0.8
	 * @return \WP_Post
	 */
	private function to_wp_post( $post ) {
		$obj = (object) $post;

		/*
		 * These will only be set if Distributor is active on the other side
		 */
		$obj->original_site_name = ( ! empty( $post['distributor_original_site_name'] ) ) ? $post['distributor_original_site_name'] : null;
		$obj->original_site_url  = ( ! empty( $post['distributor_original_site_url'] ) ) ? $post['distributor_original_site_url'] : null;

		// Unset these as they are renamed above.
		unset( $obj->distributor_original_site_name );
		unset( $obj->distributor_original_site_url );


		/**
		 * Filter the post item.
		 *
		 * @since 1.0
		 * @hook dt_item_mapping
		 *
		 * @param  {WP_Post}            $obj  The WP_Post that is being pushed.
		 * @param  {ExternalConnection} $this The external connection the post concerns.
		 *
		 * @return {WP_Post} The WP_Post that is being pushed.
		 */
		$post_object = apply_filters( 'dt_item_mapping', new \WP_Post( $obj ), $post, $this );

		return $post_object;
	}

	/**
	 * Setup actions and filters that are need on every page load
	 *
	 * @since 1.0
	 */
	public static function bootstrap() {
	}

	/**
	 * Setup canonicalization on front end
	 *
	 * @since  1.0
	 * @deprecated 2.0.0
	 */
	public static function canonicalize_front_end() {
		_deprecated_function( __METHOD__, '2.0.0' );
	}

	/**
	 * Override author with site name on distributed post
	 *
	 * @since  1.0
	 * @deprecated 2.0.0 Use Distributor\Hooks\filter_author_link instead.
	 *
	 * @param  string $link Author link.
	 * @param  int    $author_id Author ID.
	 * @param  string $author_nicename Author name.
	 * @return string
	 */
	public static function author_posts_url_distributed( $link, $author_id, $author_nicename ) {
		_deprecated_function( __METHOD__, '2.0.0', 'Distributor\Hooks\filter_author_link' );
		return \Distributor\Hooks\filter_author_link( $link );
	}

	/**
	 * Override author with site name on distributed post
	 *
	 * @since  1.0
	 * @deprecated 2.0.0 Use Distributor\Hooks\filter_the_author instead.
	 *
	 * @param  string $author Author name.
	 * @return string
	 */
	public static function the_author_distributed( $author ) {
		_deprecated_function( __METHOD__, '2.0.0', 'Distributor\Hooks\filter_the_author' );
		return \Distributor\Hooks\filter_the_author( $author );
	}

	/**
	 * Make sure canonical url header is outputted
	 *
	 * @since  1.0
	 * @deprecated 2.0.0 Use Distributor\Hooks\get_canonical_url instead.
	 *
	 * @param  string $canonical_url Canonical URL.
	 * @param  object $post Post object.
	 * @return string
	 */
	public static function canonical_url( $canonical_url, $post ) {
		_deprecated_function( __METHOD__, '2.0.0', 'Distributor\Hooks\get_canonical_url' );
		return \Distributor\Hooks\get_canonical_url( $canonical_url, $post );
	}

	/**
	 * Handles the canonical URL change for distributed content when Yoast SEO is in use
	 *
	 * @since  1.0
	 * @deprecated 2.0.0 Use Distributor\Hooks\wpseo_canonical instead.
	 *
	 * @param string $canonical_url The Yoast WPSEO deduced canonical URL
	 * @return string $canonical_url The updated distributor friendly URL
	 */
	public static function wpseo_canonical_url( $canonical_url ) {
		_deprecated_function( __METHOD__, '2.0.0', 'Distributor\Hooks\wpseo_canonical' );
		$presentation = false;
		if ( is_singular() ) {
			$source       = get_post();
			$presentation = (object) array( 'source' => $source );
		}
		return \Distributor\Hooks\wpseo_canonical( $canonical_url, $presentation );
	}

	/**
	 * Handles the og:url change for distributed content when Yoast SEO is in use
	 *
	 * @deprecated 2.0.0 Use Distributor\Hooks\wpseo_opengraph_url instead.
	 *
	 * @param string $og_url The Yoast WPSEO deduced OG URL which is a result of wpseo_canonical_url
	 * @return string $og_url The updated distributor friendly URL
	 */
	public static function wpseo_opengraph_url( $og_url ) {
		_deprecated_function( __METHOD__, '2.0.0', 'Distributor\Hooks\wpseo_opengraph_url' );
		$presentation = false;
		if ( is_singular() ) {
			$source       = get_post();
			$presentation = (object) array( 'source' => $source );
		}
		return \Distributor\Hooks\wpseo_opengraph_url( $og_url, $presentation );
	}
}