Source: auto-distribute.php

<?php
/**
 * Auto-distribution functionality for Distributor.
 *
 * @package  distributor
 */

namespace Distributor\AutoDistribute;

/**
 * Setup actions and filters.
 *
 * Runs on the `plugins_loaded, 20` hook.
 *
 * @since 2.2.0
 */
function setup() {
	$n = function( $function ) {
		return __NAMESPACE__ . "\\$function";
	};

	if ( ! enabled() ) {
		return;
	}

	add_action( 'publish_post', $n( 'schedule_post_for_auto_distribution' ), 10 );
	add_action( 'dt_auto_distribute', $n( 'distribute_post_to_all_connections' ), 10, 2 );
}

/**
 * Whether auto-distribution is enabled.
 *
 * Default is false, see the filter `dt_auto_distribution_enabled` to enable it.
 *
 * @since 2.2.0
 *
 * @return bool Whether the auto-distribution feature is enabled.
 */
function enabled() {
	/**
	 * Filter whether auto-distribution is enabled.
	 *
	 * To enable auto-distribution, you can use the code:
	 * ```php
	 *     add_filter( 'dt_auto_distribution_enabled', '__return_true' );
	 * ```
	 *
	 * Enabling auto-distribution will automatically distribute posts upon publication
	 * to all network and external connections that the post had not already been distributed
	 * to. These posts will be distributed as published posts, not drafts.
	 *
	 * @since 2.2.0
	 * @hook dt_auto_distribution_enabled
	 *
	 * @param {bool} $enabled Whether the auto-distribution feature is enabled. Default false.
	 *
	 * @return {bool} Whether the auto-distribution feature is enabled.
	 */
	return apply_filters( 'dt_auto_distribution_enabled', false );
}

/**
 * Return the default post status for auto-distributed posts.
 *
 * This is the default status that will be applied to auto-distributed posts
 * on the distribution site.
 *
 * @since 2.2.0
 *
 * @param int|\WP_Post $post Post ID or WP_Post object for the post being pushed.
 * @return string Default status for auto-distributed posts.
 *                Default is 'publish'.
 */
function default_post_status( $post ) {
	$post = get_post( $post );
	/**
	 * Filter the default status for auto-distributed posts.
	 *
	 * @since 2.2.0
	 * @hook dt_auto_distribution_default_status
	 *
	 * @param {string}  $status Default status for auto-distributed posts. Default 'publish'.
	 * @param {WP_Post} $post   Post object for the post being pushed.
	 *
	 * @return {string} Default status for auto-distributed posts.
	 */
	return apply_filters( 'dt_auto_distribution_default_status', 'publish', $post );
}

/**
 * Determine if a post should be auto-distributed.
 *
 * This function checks if the post should be auto-distributed based on filters.
 *
 * @since 2.2.0
 *
 * @param int|\WP_Post $post             Post ID or WP_Post object for the post being pushed.
 * @param int          $user_id          User ID of the user pushing the post.
 * @param string       $connection_type  Type of connection ('external' or 'internal').
 * @param int          $connection_id    Connection ID.
 *
 * @return bool Whether the post should be auto-distributed.
 */
function auto_distribute_post( $post, $user_id, $connection_type, $connection_id ) {
	$post = get_post( $post );
	if ( ! $post ) {
		return false;
	}

	/**
	 * Filter to determine if a post should be auto-distributed.
	 *
	 * @since 2.2.0
	 * @hook dt_auto_distribute_post
	 *
	 * @param {bool}    $should_distribute Whether the post should be auto-distributed.
	 * @param {WP_Post} $post              WP_Post object for the post being pushed.
	 * @param {int}     $user_id           User ID of the user pushing the post.
	 * @param {string}  $connection_type   Type of connection ('external' or 'internal').
	 * @param {int}     $connection_id     Connection ID.
	 *
	 * @return {bool} Whether the post should be auto-distributed.
	 */
	return apply_filters( 'dt_auto_distribute_post', true, $post, $user_id, $connection_type, $connection_id );
}

/**
 * Return supported post types.
 *
 * By default, this is post and page but that
 * value can be filtered.
 *
 * @since 2.2.0
 *
 * @return string[] Array of post types that can be auto-distributed.
 */
function auto_distribute_supported_post_types() {
	$post_types = array( 'post', 'page' );
	/**
	 * Filter the post types that are auto-distributable.
	 *
	 * @since 2.2.0
	 * @hook auto_distribute_supported_post_types
	 *
	 * @param {string[]} $post_types Array of post types that can be auto-distributed.
	 *                               Default is array( 'post', 'page' ).
	 * @return {string[]} Array of post types that can be auto-distributed.
	 */
	return apply_filters( 'auto_distribute_supported_post_types', $post_types );
}

/**
 * Schedule a post for auto distribution.
 *
 * Create a scheduled event to distribute the post to all connections,
 * including both internal and external connections.
 *
 * @param int|WP_Post $post     Post ID or WP_Post object.
 * @param int         $user_id User ID of the post author. Defaults to current user.
 */
function schedule_post_for_auto_distribution( $post = 0, $user_id = 0 ) {
	$post = get_post( $post );

	if ( ! $post ) {
		return;
	}

	if ( ! in_array( $post->post_type, auto_distribute_supported_post_types(), true ) ) {
		// If the post type is not supported, do not schedule for distribution.
		return;
	}

	if ( get_post_meta( $post->ID, 'dt_original_post_url', true ) ) {
		// Avoid re-distributing posts that are themselves distributed
		return;
	}

	$user_id = $user_id ? $user_id : get_current_user_id();

	if ( ! wp_next_scheduled( 'dt_auto_distribute', [ $post->ID, $user_id ] ) ) {
		wp_schedule_single_event( time(), 'dt_auto_distribute', [ $post->ID, $user_id ] );
	}
}

/**
 * Distribute a post to all connections.
 *
 * This function retrieves all connections for the given post and user,
 * and distributes the post to each connection that is not already syndicated.
 *
 * @param int|WP_Post $post     Post ID or WP_Post object.
 * @param int         $user_id User ID of the post author.
 */
function distribute_post_to_all_connections( $post = 0, $user_id = 0 ) {
	$post = get_post( $post );

	if ( ! $post || ! $user_id ) {
		return;
	}

	if ( ! in_array( $post->post_type, auto_distribute_supported_post_types(), true ) ) {
		// If the post type is not supported, do not auto distribute.
		return;
	}

	$connections = get_connections( $post->ID, $user_id );
	if ( empty( $connections ) ) {
		return;
	}

	foreach ( $connections as $connection ) {
		if ( $connection['syndicated'] ) {
			continue;
		}

		$connection_id   = $connection['id'];
		$connection_type = $connection['type'];

		distribute_post( $post->ID, $user_id, $connection_id, $connection_type );
	}
}

/**
 * Distribute a post to a specific connection.
 *
 * This function pushes the post to the specified connection and updates
 * the connection map in the post meta.
 *
 * @param int    $post_id         Post ID.
 * @param int    $user_id         User ID of the post author.
 * @param int    $connection_id   Connection ID.
 * @param string $connection_type Type of connection ('external' or 'internal').
 */
function distribute_post( $post_id = 0, $user_id = 0, $connection_id = 0, $connection_type = '' ) {
	$post = get_post( $post_id );

	if ( ! $post || ! $user_id || ! $connection_id || ! $connection_type ) {
		return;
	}

	if ( ! auto_distribute_post( $post, $user_id, $connection_type, $connection_id ) ) {
		// The post should not be auto-distributed, do not proceed.
		return;
	}

	// Make sure we have a proper user set up
	if ( ! $user_id ) {
		$user_id = get_current_user_id();
	}
	wp_set_current_user( $user_id );

	if ( 'external' === $connection_type ) {
		$connection = \Distributor\ExternalConnection::instantiate( $connection_id );
	} elseif ( 'internal' === $connection_type ) {
		$connection = new \Distributor\InternalConnections\NetworkSiteConnection( get_site( $connection_id ) );
	} else {
		return; // Invalid connection type
	}

	$post_id        = $post->ID;
	$connection_map = get_post_meta( intval( $post_id ), 'dt_connection_map', true );
	if ( ! is_array( $connection_map ) ) {
		$connection_map = [];
	}
	if ( empty( $connection_map['internal'] ) ) {
		$connection_map['internal'] = [];
	}
	if ( empty( $connection_map['external'] ) ) {
		$connection_map['external'] = [];
	}
	$args        = array(
		'post_status' => default_post_status( $post ),
	);
	$remote_post = $connection->push( $post_id, $args );
	if ( ! is_wp_error( $remote_post ) && ! empty( $remote_post['id'] ) ) {
		// Store the connection mapping
		$connection_map[ $connection_type ][ $connection_id ] = [
			'post_id' => (int) $remote_post['id'],
			'time'    => time(),
		];

		// Update the post meta with the new connection map
		update_post_meta( intval( $post_id ), 'dt_connection_map', $connection_map );
	}
}

/**
 * Get all connections
 *
 * @param integer $post_id Post ID.
 * @param integer $user_id User ID.
 * @return array
 */
function get_connections( $post_id = 0, $user_id = 0 ) {
	$external = get_external_connections( $post_id, $user_id );
	$internal = get_internal_connections( $post_id, $user_id );

	return array_merge( $external, $internal );
}

/**
 * Get all external connections
 *
 * @param integer $post_id Post ID.
 * @param integer $user_id User ID.
 * @return array
 */
function get_external_connections( $post_id = 0, $user_id = 0 ) {
	$connection_objects = [];
	$connections        = new \WP_Query(
		[
			'post_type'      => 'dt_ext_connection',
			'fields'         => 'ids',
			'no_found_rows'  => true,
			/**
			 * Filter the maximum number of external connections to load.
			 *
			 * Modify the maximum number of external connection post types are
			 * queried with requesting the post type.
			 *
			 * @hook dt_external_connections_per_page
			 *
			 * @since 2.2.0
			 *
			 * @param {int} $max_connections The maximum number of external connections to load.
			 * @return {int} The maximum number of external connections to load.
			 */
			'posts_per_page' => apply_filters( 'dt_external_connections_per_page', 200 ), // @codingStandardsIgnoreLine This high pagination limit is purposeful
		]
	);
	// Prime posts and post meta caches. No terms for external connections.
	_prime_post_caches( $connections->posts, false, true );

	// Get our current connection mapping
	$connection_map = (array) get_post_meta( $post_id, 'dt_connection_map', true );
	if ( empty( $connection_map['external'] ) ) {
		$connection_map['external'] = [];
	}

	// Make sure we have a proper user set up
	if ( ! $user_id ) {
		$user_id = get_current_user_id();
	}
	wp_set_current_user( $user_id );

	foreach ( $connections->posts as $connection_id ) {
		$connection_type = get_post_meta( $connection_id, 'dt_external_connection_type', true );

		if ( empty( \Distributor\Connections::factory()->get_registered()[ $connection_type ] ) ) {
			continue;
		}

		$connection_status = get_post_meta( $connection_id, 'dt_external_connections', true );
		$allowed_roles     = get_post_meta( $connection_id, 'dt_external_connection_allowed_roles', true );

		if ( empty( $allowed_roles ) ) {
			$allowed_roles = array( 'administrator', 'editor' );
		}

		// Make sure we have a proper connection status
		if ( empty( $connection_status ) || empty( $connection_status['can_get'] ) ) {
			continue;
		}

		// Make sure we have no connection errors
		if ( ! empty( $connection_status['errors'] ) && ! empty( $connection_status['errors']['no_distributor'] ) ) {
			continue;
		}

		// Make sure this post type is supported
		if ( ! in_array( get_post_type( $post_id ), $connection_status['can_post'], true ) ) {
			continue;
		}

		// If not admin lets make sure the current user can push to this connection
		if ( ! current_user_can( apply_filters( 'dt_push_capabilities', 'manage_options' ) ) ) {
			$current_user_roles = (array) wp_get_current_user()->roles;

			if ( count( array_intersect( $current_user_roles, $allowed_roles ) ) < 1 ) {
				continue;
			}
		}

		$connection = \Distributor\ExternalConnection::instantiate( $connection_id );

		if ( ! is_wp_error( $connection ) ) {
			$connection_objects[ 'external' . $connection->id ] = [
				'type'       => 'external',
				'id'         => $connection->id,
				'url'        => $connection->base_url,
				'name'       => $connection->name,
				'syndicated' => ( ! empty( $connection_map['external'][ (int) $connection_id ] ) ) ? true : false,
			];
		}
	}

	return $connection_objects;
}

/**
 * Get all internal connections
 *
 * @param integer $post_id Post ID.
 * @param integer $user_id User ID.
 * @return array
 */
function get_internal_connections( $post_id = 0, $user_id = 0 ) {
	$connection_objects = [];

	if ( ! is_multisite() || empty( \Distributor\Connections::factory()->get_registered()['networkblog'] ) ) {
		return $connection_objects;
	}

	// Get our current connection mapping
	$connection_map = (array) get_post_meta( $post_id, 'dt_connection_map', true );
	if ( empty( $connection_map['internal'] ) ) {
		$connection_map['internal'] = [];
	}

	// Make sure we have a proper user set up
	if ( ! $user_id ) {
		$user_id = get_current_user_id();
	}
	wp_set_current_user( $user_id );

	$sites = \Distributor\InternalConnections\NetworkSiteConnection::build_available_authorized_sites( $user_id, 'push' );

	foreach ( $sites as $site_array ) {
		if ( ! in_array( get_post_type( $post_id ), $site_array['post_types'], true ) ) {
			continue;
		}

		$connection = new \Distributor\InternalConnections\NetworkSiteConnection( $site_array['site'] );

		$syndicated = false;
		if ( ! empty( $connection_map['internal'][ (int) $connection->site->blog_id ] ) ) {
			switch_to_blog( $connection->site->blog_id );
			$syndicated = get_permalink( $connection_map['internal'][ (int) $connection->site->blog_id ]['post_id'] );
			restore_current_blog();

			if ( empty( $syndicated ) ) {
				$syndicated = true; // In case it was deleted
			}
		}

		$connection_objects[ 'internal' . $connection->site->blog_id ] = [
			'type'       => 'internal',
			'id'         => $connection->site->blog_id,
			'url'        => untrailingslashit( preg_replace( '#(https?:\/\/|www\.)#i', '', get_site_url( $connection->site->blog_id ) ) ),
			'name'       => $connection->site->blogname,
			'syndicated' => $syndicated,
		];
	}

	return $connection_objects;
}