<?php
/**
* Network site functionality
*
* @package distributor
*/
namespace Distributor\InternalConnections;
use \Distributor\DistributorPost;
use \Distributor\Connection as Connection;
use Distributor\Utils;
use \WP_Site as WP_Site;
/**
* A network site connection let's you push and pull content within your blog
*/
class NetworkSiteConnection extends Connection {
/**
* Current site
*
* @var WP_Site
*/
public $site;
/**
* Connection slug
*
* @var string
*/
public static $slug = 'networkblog';
/**
* Default post type to pull.
*
* @var string
*/
public $pull_post_type;
/**
* Default post types supported.
*
* @var string
*/
public $pull_post_types;
/**
* Set up network site connection
*
* @param WP_Site $site Site object.
* @since 0.8
*/
public function __construct( WP_Site $site ) {
$this->site = $site;
}
/**
* Push post to another internal site
*
* @param int|WP_Post $post Post or Post ID to push. Required.
* @param array $args {
* Optional. Array of push arguments
*
* @type int $remote_post_id Post ID on remote site. If not provided,
* a new post will be created.
* @type string $post_status The post status to use on the remote site.
* Ignored when updating posts. Default 'publish'.
* }
* @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' ) );
}
$post_id = $post->ID;
$args = wp_parse_args(
$args,
array(
'post_status' => 'publish',
)
);
$dt_post = new DistributorPost( $post_id );
$dt_post_args = $dt_post->to_insert( $args );
$original_blog_id = get_current_blog_id();
$output = array();
$post = Utils\prepare_post( get_post( $post_id ) );
$update = false;
$post_meta = $dt_post->get_meta();
$post_terms = $dt_post->get_terms();
$post_media = $dt_post->get_media();
switch_to_blog( $this->site->blog_id );
if ( ! empty( $args['remote_post_id'] ) && ! get_post( $args['remote_post_id'] ) ) {
// The remote post ID is not valid.
restore_current_blog();
return new \WP_Error( 'dt_invalid_remote_post_id', __( 'Invalid remote post ID', 'distributor' ) );
}
// If we have a remote post ID, we're updating.
if ( ! empty( $args['remote_post_id'] ) ) {
$update = true;
}
add_filter( 'wp_insert_post_data', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'maybe_set_modified_date' ), 10, 2 );
// Filter documented in includes/classes/ExternalConnections/WordPressExternalConnection.php
$new_post_args = Utils\post_args_allow_list( apply_filters( 'dt_push_post_args', $dt_post_args, $post, $args, $this ) );
if ( $update ) {
$new_post_id = wp_update_post( wp_slash( $new_post_args ), true );
} else {
$new_post_id = wp_insert_post( wp_slash( $new_post_args ) );
}
remove_filter( 'wp_insert_post_data', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'maybe_set_modified_date' ), 10, 2 );
if ( is_wp_error( $new_post_id ) ) {
/**
* Fires after a post is pushed via Distributor before `restore_current_blog()`.
*
* @since 1.2.2
* @deprecated 2.0.0 The dt_push_post action has been deprecated. Please use dt_push_network_post or dt_push_external_post instead.
* @hook dt_push_post
*
* @param {int} $new_post_id The newly created post.
* @param {int} $post_id The original post.
* @param {array} $args The arguments passed into wp_insert_post.
* @param {Connection} $this The Distributor connection being pushed to.
*/
do_action_deprecated(
'dt_push_post',
array( $new_post_id, $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 `restore_current_blog()`.
*
* @since 2.0.0
* @hook dt_push_network_post
*
* @param {int} $new_post_id The newly created post.
* @param {int} $post_id The original post.
* @param {array} $args The arguments passed into wp_insert_post.
* @param {NetworkSiteConnection} $this The Distributor connection being pushed to.
*/
do_action( 'dt_push_network_post', $new_post_id, $post_id, $args, $this );
restore_current_blog();
return $new_post_id;
}
$output['id'] = $new_post_id;
update_post_meta( $new_post_id, 'dt_original_blog_id', absint( $original_blog_id ) );
update_post_meta( $new_post_id, 'dt_syndicate_time', absint( time() ) );
/**
* Allow bypassing of all media processing.
*
* @hook dt_push_post_media
*
* @param {bool} true If Distributor should push the post media.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_media List of media items attached to the post, formatted by {@link \Distributor\Utils\prepare_media()}.
* @param {int} $post_id The original post ID.
* @param {array} $args The arguments passed into wp_insert_post.
* @param {Connection} $this The distributor connection being pushed to.
*
* @return {bool} If Distributor should push the post media.
*/
if ( apply_filters( 'dt_push_post_media', true, $new_post_id, $post_media, $post_id, $args, $this ) ) {
Utils\set_media( $new_post_id, $post_media, [ 'use_filesystem' => true ] );
};
$media_errors = get_transient( 'dt_media_errors_' . $new_post_id );
if ( $media_errors ) {
$output['push-errors'] = $media_errors;
delete_transient( 'dt_media_errors_' . $new_post_id );
}
/**
* Allow bypassing of all term processing.
*
* @hook dt_push_post_terms
*
* @param {bool} true If Distributor should push the post terms.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_terms Terms attached to the post, formatted by {@link \Distributor\Utils\prepare_taxonomy_terms()}.
* @param {int} $post_id The original post ID.
* @param {array} $args The arguments passed into wp_insert_post.
* @param {Connection} $this The distributor connection being pushed to.
*
* @return {bool} If Distributor should push the post terms.
*/
if ( apply_filters( 'dt_push_post_terms', true, $new_post_id, $post_terms, $post_id, $args, $this ) ) {
Utils\set_taxonomy_terms( $new_post_id, $post_terms );
}
/**
* Allow bypassing of all meta processing.
*
* @hook dt_push_post_meta
*
* @param {bool} true If Distributor should push the post meta.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_meta Meta attached to the post, formatted by {@link \Distributor\Utils\prepare_meta()}.
* @param {int} $post_id The original post ID.
* @param {array} $args The arguments passed into wp_insert_post.
* @param {Connection} $this The distributor connection being pushed to.
*
* @return {bool} If Distributor should push the post meta.
*/
if ( apply_filters( 'dt_push_post_meta', true, $new_post_id, $post_meta, $post_id, $args, $this ) ) {
$post_meta = $this->exclude_additional_meta_data( $post_meta );
Utils\set_meta( $new_post_id, $post_meta );
}
/** This filter is documented in includes/classes/InternalConnections/NetworkSiteConnection.php */
do_action_deprecated(
'dt_push_post',
array( $new_post_id, $post_id, $args, $this ),
'2.0.0',
'dt_push_network_post|dt_push_external_post',
esc_html__( 'The dt_push_post action has been deprecated. Please use dt_push_network_post or dt_push_external_post instead.', 'distributor' )
);
/** This filter is documented in includes/classes/InternalConnections/NetworkSiteConnection.php */
do_action( 'dt_push_network_post', $new_post_id, $post_id, $args, $this );
restore_current_blog();
return $output;
}
/**
* 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 Array of items to pull.
* @since 0.8
* @return array
*/
public function pull( $items ) {
global $dt_pull_messages;
$created_posts = array();
foreach ( $items as $item_array ) {
$update = false;
$insert_args = array();
if ( ! empty( $item_array['post_status'] ) ) {
$insert_args['post_status'] = $item_array['post_status'];
}
if ( ! empty( $item_array['post_id'] ) ) {
$insert_args['remote_post_id'] = $item_array['post_id'];
$update = true;
}
$post = $this->remote_get( [ 'id' => $item_array['remote_post_id'] ], $insert_args );
if ( is_wp_error( $post ) ) {
$created_posts[] = $post;
continue;
}
$post_props = $post;
$post_array = $post_props;
$current_blog_id = get_current_blog_id();
if ( ! empty( $post_props['meta']['dt_connection_map'] ) ) {
foreach ( $post_props['meta']['dt_connection_map'] as $connection_type => $distributed ) {
$distributed = maybe_unserialize( $distributed );
if ( 'internal' === $connection_type && array_key_exists( $current_blog_id, $distributed ) ) {
$dt_pull_messages['duplicated'] = 1;
continue 2;
}
}
}
add_filter( 'wp_insert_post_data', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'maybe_set_modified_date' ), 10, 2 );
// Filter documented in includes/classes/ExternalConnections/WordPressExternalConnection.php
$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_id = wp_update_post( wp_slash( $new_post_args ) );
} else {
$new_post_id = wp_insert_post( wp_slash( $new_post_args ) );
}
remove_filter( 'wp_insert_post_data', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'maybe_set_modified_date' ), 10, 2 );
if ( ! is_wp_error( $new_post_id ) ) {
update_post_meta( $new_post_id, 'dt_original_blog_id', absint( $this->site->blog_id ) );
update_post_meta( $new_post_id, 'dt_syndicate_time', absint( time() ) );
update_post_meta( $new_post_id, 'dt_original_post_id', absint( $new_post_args['meta_input']['dt_original_post_id'] ) );
update_post_meta( $new_post_id, 'dt_original_post_url', wp_slash( sanitize_url( $new_post_args['meta_input']['dt_original_post_url'] ) ) );
/**
* Allow bypassing of all media processing.
*
* @hook dt_pull_post_media
*
* @param {bool} true If Distributor should set the post media.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_media List of media items attached to the post, formatted by {@link \Distributor\Utils\prepare_media()}.
* @param {int} $remote_post_id The original post ID.
* @param {array} $post_array The arguments passed into wp_insert_post.
* @param {NetworkSiteConnection} $this The Distributor connection being pulled from.
*
* @return {bool} If Distributor should set the post media.
*/
if ( apply_filters( 'dt_pull_post_media', true, $new_post_id, $post['media'], $item_array['remote_post_id'], $post_array, $this ) ) {
\Distributor\Utils\set_media( $new_post_id, $post['media'], [ 'use_filesystem' => true ] );
};
/**
* Allow bypassing of all terms processing.
*
* @hook dt_pull_post_terms
*
* @param {bool} true If Distributor should set the post terms.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_terms List of terms items attached to the post, formatted by {@link \Distributor\Utils\prepare_taxonomy_terms()}.
* @param {int} $remote_post_id The original post ID.
* @param {array} $post_array The arguments passed into wp_insert_post.
* @param {NetworkSiteConnection} $this The Distributor connection being pulled from.
*
* @return {bool} If Distributor should set the post terms.
*/
if ( apply_filters( 'dt_pull_post_terms', true, $new_post_id, $post['terms'], $item_array['remote_post_id'], $post_array, $this ) ) {
\Distributor\Utils\set_taxonomy_terms( $new_post_id, $post['terms'] );
}
/**
* Allow bypassing of all meta processing.
*
* @hook dt_pull_post_meta
*
* @param {bool} true If Distributor should set the post meta.
* @param {int} $new_post_id The newly created post ID.
* @param {array} $post_meta List of meta items attached to the post, formatted by {@link \Distributor\Utils\prepare_meta()}.
* @param {int} $remote_post_id The original post ID.
* @param {array} $post_array The arguments passed into wp_insert_post.
* @param {NetworkSiteConnection} $this The Distributor connection being pulled from.
*
* @return {bool} If Distributor should set the post meta.
*/
if ( apply_filters( 'dt_pull_post_meta', true, $new_post_id, $post['meta'], $item_array['remote_post_id'], $post_array, $this ) ) {
$post_meta = $this->exclude_additional_meta_data( $post['meta'] );
\Distributor\Utils\set_meta( $new_post_id, $post_meta );
}
}
switch_to_blog( $this->site->blog_id );
$connection_map = get_post_meta( $item_array['remote_post_id'], 'dt_connection_map', true );
if ( empty( $connection_map ) ) {
$connection_map = [
'internal' => [],
'external' => [],
];
}
if ( empty( $connection_map['internal'] ) ) {
$connection_map['internal'] = [];
}
$connection_map['internal'][ $current_blog_id ] = [
'post_id' => (int) $new_post_id,
'time' => time(),
];
update_post_meta( $item_array['remote_post_id'], 'dt_connection_map', $connection_map );
restore_current_blog();
/**
* Allow the sync'ed post to be updated via a REST request get the rendered content.
*
* @hook dt_pull_post_apply_rendered_content
*
* @param {bool} false Apply rendered content after a pull? Defaults to false.
* @param {int} $new_post_id The new post ID.
* @param {Connection} $this The Distributor connection pulling the post.
* @param {array} $post_array The post array used to create the new post.
*
* @return {bool} Whether to apply rendered content after a pull.
*/
if ( apply_filters( 'dt_pull_post_apply_rendered_content', false, $new_post_id, $this, $post_array ) ) {
$this->update_content_via_rest( $new_post_id );
}
/**
* Action triggered when a post is pulled via distributor.
* Fires after a post is pulled via Distributor and after `restore_current_blog()`.
*
* @since 1.0
* @hook dt_pull_post
*
* @param {int} $new_post_id The new post ID that was pulled.
* @param {Connection} $this The Distributor connection pulling the post.
* @param {array} $post_array The original post data retrieved via the connection.
*/
do_action( 'dt_pull_post', $new_post_id, $this, $post_array );
$created_posts[] = $new_post_id;
}
return $created_posts;
}
/**
* Log a sync. Unfortunately have to use options. We store like this:
*
* {
* original_connection_id: {
* old_post_id: new_post_id (false means skipped)
* }
* }
*
* This let's us grab all the IDs of posts we've PULLED from a given site
*
* @param array $item_id_mappings Mapping to log; key = origin post ID, value = new post ID.
* @param int $blog_id Blog ID
* @param boolean $overwrite Whether to overwrite the sync log for this site. Default false.
* @since 0.8
*/
public function log_sync( array $item_id_mappings, $blog_id = 0, $overwrite = false ) {
$blog_id = 0 === $blog_id ? $this->site->blog_id : $blog_id;
$current_site_log = [];
if ( false === $overwrite ) {
$current_site_log = $this->get_sync_log( $blog_id );
}
foreach ( $item_id_mappings as $old_item_id => $new_item_id ) {
if ( empty( $new_item_id ) || is_wp_error( $new_item_id ) ) {
$current_site_log[ $old_item_id ] = false;
} else {
$current_site_log[ $old_item_id ] = (int) $new_item_id;
}
}
$sync_log[ $blog_id ] = $current_site_log;
update_option( 'dt_sync_log', $sync_log );
// Action documented in includes/classes/ExternalConnection.php.
do_action( 'dt_log_sync', $item_id_mappings, $sync_log, $this );
}
/**
* Return the sync log for a specific site
*
* @param int $blog_id Blog ID
* @return array
*/
public function get_sync_log( $blog_id = 0 ) {
$blog_id = 0 === $blog_id ? $this->site->blog_id : $blog_id;
$sync_log = get_option( 'dt_sync_log', [] );
$current_site_log = [];
if ( ! empty( $sync_log[ $blog_id ] ) ) {
$current_site_log = $sync_log[ $blog_id ];
}
return $current_site_log;
}
/**
* Get the available post types.
*
* @since 1.3
* @return array
*/
public function get_post_types() {
switch_to_blog( $this->site->blog_id );
$post_types = Utils\distributable_post_types( 'objects' );
restore_current_blog();
return $post_types;
}
/**
* Remotely get posts so we can list them for pulling
*
* @since 0.8
* @since 2.0.0 Added $new_post_args parameter.
*
* @param array $args Array of args for getting.
* @param array $new_post_args {
* Array of args for creating new post.
*
* @type string $post_status Post status for new post.
* }
* @return array|WP_Post|bool
*/
public function remote_get( $args = array(), $new_post_args = array() ) {
$id = ( empty( $args['id'] ) ) ? false : $args['id'];
switch_to_blog( $this->site->blog_id );
$query_args = array();
if ( empty( $id ) ) {
if ( isset( $args['post__in'] ) ) {
if ( empty( $args['post__in'] ) ) {
// If post__in is empty, we can just stop right here
restore_current_blog();
// Filter documented in includes/classes/ExternalConnections/WordPressExternalConnection.php
return apply_filters(
'dt_remote_get',
[
'items' => array(),
'total_items' => 0,
],
$args,
$this
);
}
$query_args['post__in'] = $args['post__in'];
} elseif ( isset( $args['post__not_in'] ) ) {
$query_args['post__not_in'] = $args['post__not_in'];
}
$query_args['post_type'] = ( empty( $args['post_type'] ) ) ? 'post' : $args['post_type'];
$query_args['post_status'] = ( empty( $args['post_status'] ) ) ? [ 'publish', 'draft', 'private', 'pending', 'future' ] : $args['post_status'];
$query_args['posts_per_page'] = ( empty( $args['posts_per_page'] ) ) ? get_option( 'posts_per_page' ) : $args['posts_per_page'];
$query_args['paged'] = ( empty( $args['paged'] ) ) ? 1 : $args['paged'];
if ( isset( $args['meta_query'] ) ) {
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$query_args['meta_query'] = $args['meta_query'];
}
if ( isset( $args['s'] ) ) {
$query_args['s'] = urldecode( $args['s'] );
}
if ( ! empty( $args['orderby'] ) ) {
$query_args['orderby'] = $args['orderby'];
}
if ( ! empty( $args['order'] ) ) {
$query_args['order'] = $args['order'];
}
// Filter documented in includes/classes/ExternalConnections/WordPressExternalConnection.php
$posts_query = new \WP_Query( apply_filters( 'dt_remote_get_query_args', $query_args, $args, $this ) );
$posts = $posts_query->posts;
$formatted_posts = [];
foreach ( $posts as $post ) {
$formatted_posts[] = Utils\prepare_post( $post );
}
restore_current_blog();
// Filter documented in /includes/classes/ExternalConnections/WordPressExternalConnection.php.
return apply_filters(
'dt_remote_get',
[
'items' => $formatted_posts,
'total_items' => $posts_query->found_posts,
],
$args,
$this
);
} else {
$post = get_post( $id );
if ( empty( $post ) ) {
$formatted_post = false;
} else {
$dt_post = new DistributorPost( $post );
$formatted_post = $dt_post->to_insert( $new_post_args );
// The pull method requires the connection map despite it being on the deny list.
$formatted_post['meta']['dt_connection_map'] = get_post_meta( $id, 'dt_connection_map', true );
}
restore_current_blog();
// Filter documented in /includes/classes/ExternalConnections/WordPressExternalConnection.php.
return apply_filters( 'dt_remote_get', $formatted_post, $args, $this );
}
}
/**
* Setup actions and filters that are need on every page load
*
* @since 0.8
*/
public static function bootstrap() {
add_action( 'wp_after_insert_post', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'update_syndicated' ), 99 );
add_action( 'before_delete_post', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'separate_syndicated_on_delete' ) );
add_action( 'before_delete_post', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'remove_distributor_post_from_original' ) );
add_action( 'wp_trash_post', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'separate_syndicated_on_delete' ) );
add_action( 'untrash_post', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'connect_syndicated_on_untrash' ) );
add_action( 'clean_site_cache', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'set_sites_last_changed_time' ) );
add_action( 'wp_insert_site', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'set_sites_last_changed_time' ) );
add_action( 'add_user_to_blog', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'clear_authorized_sites_cache' ) );
add_action( 'remove_user_from_blog', array( '\Distributor\InternalConnections\NetworkSiteConnection', 'clear_authorized_sites_cache' ) );
}
/**
* Make the original post available for distribution when deleting a post.
*
* @param int $post_id Post ID.
* @since 1.2
*/
public static function remove_distributor_post_from_original( $post_id ) {
$original_blog_id = get_post_meta( $post_id, 'dt_original_blog_id', true );
$original_post_id = get_post_meta( $post_id, 'dt_original_post_id', true );
if ( empty( $original_blog_id ) || empty( $original_post_id ) ) {
return;
}
$blog_id = get_current_blog_id();
switch_to_blog( $original_blog_id );
$connection_map = get_post_meta( $original_post_id, 'dt_connection_map', true );
if ( ! empty( $connection_map['internal'] ) && ! empty( $connection_map['internal'][ (int) $blog_id ] ) ) {
unset( $connection_map['internal'][ (int) $blog_id ] );
update_post_meta( $original_post_id, 'dt_connection_map', $connection_map );
}
restore_current_blog();
// Mark deleted post as being skipped in the sync log.
$sync_log = get_option( 'dt_sync_log', array() );
if ( isset( $sync_log[ $original_blog_id ][ $original_post_id ] ) ) {
$sync_log[ $original_blog_id ][ $original_post_id ] = false;
update_option( 'dt_sync_log', $sync_log );
}
}
/**
* When an original is deleted, we need to let internal syndicated posts know
*
* @param int $post_id Post ID.
* @since 1.0
*/
public static function separate_syndicated_on_delete( $post_id ) {
$connection_map = get_post_meta( $post_id, 'dt_connection_map', true );
// If no connections do nothing
if ( empty( $connection_map ) || empty( $connection_map['internal'] ) ) {
return;
}
foreach ( $connection_map['internal'] as $blog_id => $post_array ) {
$site = get_site( $blog_id );
if ( ! $site || ! is_a( $site, '\WP_Site' ) ) {
continue;
}
$connection = new self( $site );
switch_to_blog( $blog_id );
$unlinked = (bool) get_post_meta( $post_array['post_id'], 'dt_unlinked', true );
update_post_meta( $post_array['post_id'], 'dt_original_post_deleted', true );
restore_current_blog();
if ( 'trash' !== get_post_status( $post_id ) && ! $unlinked ) {
$connection->push( $post_id, array( 'remote_post_id' => $post_array['post_id'] ) );
}
}
}
/**
* When an original is untrashed, we need to let internal syndicated posts know
*
* @param int $post_id Post ID.
* @since 1.0
*/
public static function connect_syndicated_on_untrash( $post_id ) {
$connection_map = get_post_meta( $post_id, 'dt_connection_map', true );
// If no connections do nothing
if ( empty( $connection_map ) || empty( $connection_map['internal'] ) ) {
return;
}
foreach ( $connection_map['internal'] as $site_id => $post_array ) {
switch_to_blog( $site_id );
delete_post_meta( $post_array['post_id'], 'dt_original_post_deleted' );
restore_current_blog();
}
}
/**
* Update syndicated post when original changes
*
* @param int|WP_Post $post Post ID or WP_Post
* depending on which action the method is hooked to.
*/
public static function update_syndicated( $post ) {
$post = get_post( $post );
$post_id = $post->ID;
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
if ( 'trash' === get_post_status( $post_id ) ) {
return;
}
$connection_map = get_post_meta( $post_id, 'dt_connection_map', true );
if ( empty( $connection_map ) || ! is_array( $connection_map ) || empty( $connection_map['internal'] ) ) {
return;
}
foreach ( $connection_map['internal'] as $blog_id => $syndicated_post ) {
// Make sure this site is still available
$site = get_site( (int) $blog_id );
if ( null === $site ) {
// If the site isn't available anymore, remove this item from the connection map
if ( ! empty( $connection_map['internal'][ (int) $blog_id ] ) ) {
unset( $connection_map['internal'][ (int) $blog_id ] );
update_post_meta( $post_id, 'dt_connection_map', $connection_map );
}
continue;
}
$connection = new self( $site );
switch_to_blog( $blog_id );
$unlinked = (bool) get_post_meta( $syndicated_post['post_id'], 'dt_unlinked', true );
restore_current_blog();
if ( ! $unlinked ) {
$connection->push( $post_id, array( 'remote_post_id' => $syndicated_post['post_id'] ) );
}
}
}
/**
* Maybe set post modified date
* On wp_insert_post, modified date is overridden by post date
*
* https://core.trac.wordpress.org/browser/tags/4.7.2/src/wp-includes/post.php#L3151
*
* @param array $data Post data.
* @param array $postarr Post args.
* @since 0.8.1
* @return array
*/
public static function maybe_set_modified_date( $data, $postarr ) {
if ( ! empty( $postarr['post_modified'] ) && ! empty( $postarr['post_modified_gmt'] ) ) {
$data['post_modified'] = $postarr['post_modified'];
$data['post_modified_gmt'] = $postarr['post_modified_gmt'];
}
return $data;
}
/**
* Find out which sites user can create post type on
*
* @since 0.8
* @since 1.3.7 Added the `$context` parameter.
*
* @param string $context The context of the authorization.
*
* @return array
*/
public static function get_available_authorized_sites( $context = null ) {
if ( ! is_multisite() ) {
return array();
}
/**
* Enable plugins to filter the authorized sites, before they are retrieved.
*
* @since 1.2
* @since 1.3.7 Added the `$context` parameter.
* @hook dt_pre_get_authorized_sites
*
* @see \Distributor\InternalConnections\NetworkSiteConnection::get_available_authorized_sites()
*
* @param {array} $authorized_sites Array of `WP_Site` object and post type objects the user can edit.
* @param {string} $context The context of the authorization.
*
* @return {array} Array of `WP_Site` object and post type objects.
*/
$authorized_sites = apply_filters( 'dt_pre_get_authorized_sites', array(), $context );
if ( ! empty( $authorized_sites ) ) {
return $authorized_sites;
}
$authorized_sites = self::build_available_authorized_sites( get_current_user_id(), $context );
/**
* Filter the array of authorized sites.
*
* @since 1.2
* @since 1.3.7 Added the `$context` parameter.
* @hook dt_authorized_sites
* @tutorial snippets
*
* @param {array} $authorized_sites An array of `WP_Site` objects and the post type objects the user can edit.
* @param {string} $context The context of the authorization.
*
* @return {array} An array of `WP_Site` objects and the post type objects.
*/
return apply_filters( 'dt_authorized_sites', $authorized_sites, $context );
}
/**
* Build the available sites a specific user is authorized to use.
*
* @param int|bool $user_id Current user ID
* @param string $context The context of the authorization. Either push or pull
* @param bool $force Force a cache clear. Default false
*
* @return array
*/
public static function build_available_authorized_sites( $user_id = false, $context = null, $force = false ) {
$user_id = ! $user_id ? get_current_user_id() : $user_id;
$last_changed = get_site_option( 'last_changed_sites' );
if ( ! $last_changed ) {
$last_changed = self::set_sites_last_changed_time();
}
$cache_key = "authorized_sites:$user_id:$context:$last_changed";
$authorized_sites = get_transient( $cache_key );
if ( $force || false === $authorized_sites ) {
$authorized_sites = array();
$sites = get_sites(
array(
'number' => 1000,
)
);
$current_blog_id = (int) get_current_blog_id();
foreach ( $sites as $site ) {
$blog_id = (int) $site->blog_id;
if ( $blog_id === $current_blog_id ) {
continue;
}
$base_url = get_site_url( $blog_id );
if ( empty( $base_url ) ) {
continue;
}
switch_to_blog( $blog_id );
$post_types = get_post_types();
$authorized_post_types = array();
foreach ( $post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
if ( current_user_can( $post_type_object->cap->create_posts ) ) {
$authorized_post_types[] = $post_type;
}
}
if ( ! empty( $authorized_post_types ) ) {
$authorized_sites[] = array(
'site' => $site,
'post_types' => $authorized_post_types,
);
}
restore_current_blog();
}
}
// Make sure we save and return an array.
$authorized_sites = ! is_array( $authorized_sites ) ? array() : $authorized_sites;
set_transient( $cache_key, $authorized_sites, 15 * MINUTE_IN_SECONDS );
return $authorized_sites;
}
/**
* Whenever site data changes, save the timestamp.
*
* WordPress stores this same information in the cache
* {@see clean_blog_cache()}, but not all environments
* will have caching enabled, so we also store it
* in a site option.
*
* @return string
*/
public static function set_sites_last_changed_time() {
$time = microtime();
update_site_option( 'last_changed_sites', $time );
return $time;
}
/**
* Clear the authorized sites cache for a specific user.
*
* @param int $user_id Current user ID.
*/
public static function clear_authorized_sites_cache( $user_id = false ) {
$last_changed = get_site_option( 'last_changed_sites' );
if ( ! $last_changed ) {
self::set_sites_last_changed_time();
} else {
delete_transient( "authorized_sites:$user_id:push:$last_changed" );
delete_transient( "authorized_sites:$user_id:pull:$last_changed" );
}
}
/**
* Setup canonicalization on front end
*
* @since 0.8
* @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 0.8
* @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 );
}
/**
* Updates a post content via a REST request after the new post is created
* in order to get the rendered content.
*
* @param int $new_post_id The new post ID that was pulled.
* @return void
*/
public function update_content_via_rest( $new_post_id ) {
$post = get_post( $new_post_id );
if ( ! is_a( $post, '\WP_Post' ) ) {
return;
}
$original_blog_id = absint( get_post_meta( $post->ID, 'dt_original_blog_id', true ) );
$original_post_id = absint( get_post_meta( $post->ID, 'dt_original_post_id', true ) );
$rest_url = false;
if ( ! empty( $original_blog_id ) && ! empty( $original_post_id ) ) {
$rest_url = Utils\get_rest_url( $original_blog_id, $original_post_id );
}
if ( empty( $rest_url ) ) {
return;
}
/**
* Allow filtering of the HTTP request args before updating content
* via a REST API call.
*
* @hook dt_update_content_via_request_args
*
* @param {array} list List of request args.
* @param {int} $new_post_id The new post ID.
* @param {NetworkSiteConnection} $this The distributor connection being pulled from.
*
* @return {array} List of filtered request args.
*/
$request = apply_filters( 'dt_update_content_via_request_args', [], $new_post_id, $this );
$response = Utils\remote_http_request( $rest_url, $request );
$body = false;
$code = wp_remote_retrieve_response_code( $response );
if ( 200 === $code ) {
$body = wp_remote_retrieve_body( $response );
}
if ( empty( $body ) ) {
return;
}
$data = json_decode( $body );
// Grab the rendered response and update the current post.
if ( is_a( $data, '\stdClass' ) && isset( $data->content, $data->content->rendered ) ) {
wp_update_post(
[
'ID' => $post->ID,
'post_content' => $data->content->rendered,
]
);
}
}
/**
* Exclude additional meta data for network distributions
*
* In network connections the featured image is set prior to the meta data.
* Excluding the `_thumbnail_id` meta from distribution prevents the meta
* data from referring to the attachment ID of the original site.
*
* @since 2.0.0
*
* @param string[] $post_meta Array of meta to include in the distribution.
* @return string[] Array of meta to include in the distribution after filtering out excluded meta.
*/
public static function exclude_additional_meta_data( $post_meta ) {
unset( $post_meta['_thumbnail_id'] );
return $post_meta;
}
}