diff --git a/assets/js/admin-pull.js b/assets/js/admin-pull.js index 4488187a4..8d2594b48 100755 --- a/assets/js/admin-pull.js +++ b/assets/js/admin-pull.js @@ -22,6 +22,7 @@ const escapeURLComponent = ( str ) => { const chooseConnection = document.getElementById( 'pull_connections' ); const choosePostType = document.getElementById( 'pull_post_type' ); const choosePostTypeBtn = document.getElementById( 'pull_post_type_submit' ); +const choosePostCategory = document.getElementById( 'pull_post_category' ); const searchField = document.getElementById( 'post-search-input' ); const searchBtn = document.getElementById( 'search-submit' ); const form = document.getElementById( 'posts-filter' ); @@ -38,7 +39,7 @@ jQuery( chooseConnection ).on( 'change', ( event ) => { document.body.className += ' ' + 'dt-loading'; } ); -if ( chooseConnection && choosePostType && form ) { +if ( chooseConnection && ( choosePostType || choosePostCategory ) && form ) { if ( choosePostTypeBtn ) { jQuery( choosePostTypeBtn ).on( 'click', ( event ) => { event.preventDefault(); @@ -99,6 +100,8 @@ const getURL = () => { const postType = escapeURLComponent( choosePostType.options[ choosePostType.selectedIndex ].value ); + const postCategory = + choosePostCategory.options[ choosePostCategory.selectedIndex ].value; const pullUrlId = escapeURLComponent( chooseConnection.options[ chooseConnection.selectedIndex ].getAttribute( 'data-pull-url-id' @@ -113,5 +116,5 @@ const getURL = () => { status = 'pulled'; } - return `${ baseURL }&pull_post_type=${ postType }&status=${ status }`; + return `${ baseURL }&pull_post_type=${ postType }&pull_post_category=${ postCategory }&status=${ status }`; }; diff --git a/includes/classes/Connection.php b/includes/classes/Connection.php index 9e22f5441..e40dcef52 100644 --- a/includes/classes/Connection.php +++ b/includes/classes/Connection.php @@ -65,6 +65,14 @@ abstract public function get_sync_log( $id ); */ abstract public function get_post_types(); + /** + * Get available post types from a connection + * + * @since 1.3 + * @return array|\WP_Error + */ + abstract public function get_post_categories(); + /** * This method is called on every page load. It's helpful for canonicalization * diff --git a/includes/classes/ExternalConnections/WordPressExternalConnection.php b/includes/classes/ExternalConnections/WordPressExternalConnection.php index 75f3a7f0c..3843c720a 100644 --- a/includes/classes/ExternalConnections/WordPressExternalConnection.php +++ b/includes/classes/ExternalConnections/WordPressExternalConnection.php @@ -72,6 +72,20 @@ class WordPressExternalConnection extends ExternalConnection { */ public $pull_post_types; + /** + * Default posts category to pull. + * + * @var string + */ + public $pull_post_category; + + /** + * Default posts categories to show in filter. + * + * @var string + */ + public $pull_post_categories; + /** * This is a utility function for parsing annoying API link headers returned by the types endpoint * @@ -168,6 +182,10 @@ public function remote_get( $args = array() ) { } } + if ( isset( $args['tax_query'] ) ) { + $query_args['tax_query'] = $args['tax_query']; + } + // 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'; @@ -682,6 +700,41 @@ public function get_post_types() { return $types_body_array; } + /** + * Get the available post categories. + * + * @since 1.3 + * @return array|\WP_Error + */ + public function get_post_categories() { + $path = self::$namespace; + + $categories_path = untrailingslashit( $this->base_url ) . '/' . $path . '/categories'; + + $categories_response = Utils\remote_http_request( + $categories_path, + $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) + ); + + if ( is_wp_error( $categories_response ) ) { + return $categories_response; + } + + if ( 404 === wp_remote_retrieve_response_code( $categories_response ) ) { + return new \WP_Error( 'bad-endpoint', esc_html__( 'Could not connect to API endpoint.', 'distributor' ) ); + } + + $categories_body = wp_remote_retrieve_body( $categories_response ); + + if ( empty( $categories_body ) ) { + return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) ); + } + + $categories_body_array = json_decode( $categories_body, true ); + + return $categories_body_array; + } + /** * Check what we can do with a given external connection (push or pull) * diff --git a/includes/classes/InternalConnections/NetworkSiteConnection.php b/includes/classes/InternalConnections/NetworkSiteConnection.php index 12d5742d2..764929ced 100644 --- a/includes/classes/InternalConnections/NetworkSiteConnection.php +++ b/includes/classes/InternalConnections/NetworkSiteConnection.php @@ -46,6 +46,20 @@ class NetworkSiteConnection extends Connection { */ public $pull_post_types; + /** + * Default posts category to pull. + * + * @var string + */ + public $pull_post_category; + + /** + * Default posts categories to show in filter. + * + * @var string + */ + public $pull_post_categories; + /** * Set up network site connection * @@ -520,6 +534,20 @@ public function get_post_types() { return $post_types; } + /** + * Get the available post categories. + * + * @since 1.3 + * @return array + */ + public function get_post_categories() { + switch_to_blog( $this->site->blog_id ); + $post_categories = Utils\distributable_categories(); + restore_current_blog(); + + return $post_categories; + } + /** * Remotely get posts so we can list them for pulling * @@ -567,6 +595,10 @@ public function remote_get( $args = array(), $new_post_args = array() ) { $query_args['post__not_in'] = $args['post__not_in']; } + if ( isset( $args['tax_query'] ) ) { + $query_args['tax_query'] = $args['tax_query']; + } + $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']; diff --git a/includes/classes/PullListTable.php b/includes/classes/PullListTable.php index ce770828d..f206020f2 100644 --- a/includes/classes/PullListTable.php +++ b/includes/classes/PullListTable.php @@ -463,10 +463,21 @@ public function prepare_items() { $current_page = $this->get_pagenum(); // Support 'View all' filtering for internal connections. - if ( empty( $connection_now->pull_post_type ) || 'all' === $connection_now->pull_post_type ) { - $post_type = wp_list_pluck( $connection_now->pull_post_types, 'slug' ); + if ( is_a( $connection_now, '\Distributor\InternalConnections\NetworkSiteConnection' ) ) { + if ( empty( $connection_now->pull_post_type ) || 'all' === $connection_now->pull_post_type ) { + $post_type = wp_list_pluck( $connection_now->pull_post_types, 'slug' ); + } else { + $post_type = $connection_now->pull_post_type; + } + + if ( empty( $connection_now->pull_post_category ) || 'all' === $connection_now->pull_post_category ) { + $post_category = wp_list_pluck( $connection_now->pull_post_categories, 'slug' ); + } else { + $post_category = $connection_now->pull_post_category; + } } else { - $post_type = $connection_now->pull_post_type; + $post_type = $connection_now->pull_post_type ? $connection_now->pull_post_type : 'post'; + $post_category = $connection_now->pull_post_category ? $connection_now->pull_post_category : 'all'; } $remote_get_args = [ @@ -480,6 +491,16 @@ public function prepare_items() { $remote_get_args['s'] = rawurlencode( $_GET['s'] ); // @codingStandardsIgnoreLine Nonce isn't required. } + if ( ! empty( $post_category ) && 'all' !== $post_category ) { + $remote_get_args['tax_query'] = [ + [ + 'taxonomy' => 'category', + 'field' => 'slug', + 'terms' => $post_category, + ], + ]; + } + if ( is_a( $connection_now, '\Distributor\ExternalConnection' ) ) { $this->sync_log = get_post_meta( $connection_now->id, 'dt_sync_log', true ); } else { @@ -626,6 +647,14 @@ public function get_bulk_actions() { * @param string $which Whether above or below the table. */ public function extra_tablenav( $which ) { + + /** + * This is to avoid the filter being displayed twice with the same HTML id. + */ + if ( 'bottom' === $which ) { + return; + } + global $connection_now; if ( is_a( $connection_now, '\Distributor\InternalConnections\NetworkSiteConnection' ) ) { @@ -634,21 +663,34 @@ public function extra_tablenav( $which ) { $connection_type = 'external'; } - if ( $connection_now && $connection_now->pull_post_types ) : + if ( $connection_now && $connection_now->pull_post_types && $connection_now->pull_post_type && $connection_now->pull_post_categories ) : ?>
- + + + $remote_post_id, 'post_type' => $post_type, 'post_status' => $post_status, + 'post_category' => 'all' === $post_category ? '' : $post_category, ]; }, $posts @@ -492,13 +494,16 @@ function dashboard() { pull_post_type = ''; $connection_now->pull_post_types = \Distributor\Utils\available_pull_post_types( $connection_now, $connection_type ); - - // Ensure we have at least one post type to pull. - $connection_now->pull_post_type = ''; if ( ! empty( $connection_now->pull_post_types ) ) { $connection_now->pull_post_type = 'all'; } + $connection_now->pull_post_category = ''; + $connection_now->pull_post_categories = \Distributor\Utils\available_pull_post_categories( $connection_now, $connection_type ); + if ( ! empty( $connection_now->pull_post_categories ) ) { + $connection_now->pull_post_category = 'all'; + } + // Set the post type we want to pull (if any) // This is either from a query param, "post" post type, or the first in the list foreach ( $connection_now->pull_post_types as $post_type ) { @@ -515,6 +520,17 @@ function dashboard() { break; } } + + foreach ( $connection_now->pull_post_categories as $post_category ) { + if ( isset( $_GET['pull_post_category'] ) ) { // @codingStandardsIgnoreLine No nonce needed here. + if ( $_GET['pull_post_category'] === $post_category['slug'] ) { // @codingStandardsIgnoreLine Comparing values, no nonce needed. + $connection_now->pull_post_category = $post_category['slug']; + break; + } + } else { + $connection_now->pull_post_category = 'all'; + } + } ?> diff --git a/includes/rest-api.php b/includes/rest-api.php index 8d60a4f7c..386dbac05 100644 --- a/includes/rest-api.php +++ b/includes/rest-api.php @@ -378,6 +378,44 @@ function( $post_type ) { 'title', ), ), + 'tax_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'description' => esc_html__( 'Filter posts by taxonomy terms.', 'distributor' ), + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'taxonomy' => array( + 'description' => esc_html__( 'Taxonomy name.', 'distributor' ), + 'type' => 'string', + ), + 'field' => array( + 'description' => esc_html__( 'Field to match terms by (slug, term_id, name).', 'distributor' ), + 'type' => 'string', + 'enum' => array( 'slug', 'term_id', 'name' ), + 'default' => 'slug', + ), + 'terms' => array( + 'description' => esc_html__( 'Term(s) to filter by.', 'distributor' ), + 'type' => array( 'array', 'string', 'integer' ), + 'items' => array( + 'type' => array( 'string', 'integer' ), + ), + ), + 'operator' => array( + 'description' => esc_html__( 'Taxonomy query operator.', 'distributor' ), + 'type' => 'string', + 'enum' => array( 'IN', 'NOT IN', 'AND', 'EXISTS', 'NOT EXISTS' ), + 'default' => 'IN', + ), + 'include_children' => array( + 'description' => esc_html__( 'Whether to include child terms.', 'distributor' ), + 'type' => 'boolean', + 'default' => true, + ), + ), + 'required' => array( 'taxonomy', 'terms' ), + ), + ), ); } @@ -669,6 +707,10 @@ function get_pull_content_list( $request ) { $args['orderby'] = 'relevance'; } + if ( isset( $request['tax_query'] ) ) { + $args['tax_query'] = $request['tax_query']; + } + if ( ! empty( $request['exclude'] ) && ! empty( $request['include'] ) ) { /* * Use only `post__in` if both `include` and `exclude` are populated. diff --git a/includes/utils.php b/includes/utils.php index e7d026a94..6bd71b085 100644 --- a/includes/utils.php +++ b/includes/utils.php @@ -285,6 +285,64 @@ function available_pull_post_types( $connection, $type ) { return $post_types; } +/** + * Get post categories available for pulling. + * + * This will compare the public post categories from a remote site + * against the public post categories from the origin site and return + * an array of categories supported on both. + * + * @param \Distributor\Connection $connection Connection object + * @param string $type Connection type + * @return array + */ +function available_pull_post_categories( $connection, $type ) { + $categories = array(); + $local_categories = array(); + $remote_categories = $connection->get_post_categories(); + $distributable_categories = distributable_categories( $remote_categories ); + + // Return empty array if the source site is not distributing any categories + if ( empty( $remote_categories ) || is_wp_error( $remote_categories ) ) { + return []; + } + + $local_categories = get_categories( [ 'hide_empty' => false ] ); + + if ( ! empty( $remote_categories ) ) { + foreach ( $remote_categories as $category ) { + $categories[] = array( + 'name' => 'external' === $type ? $category['name'] : $category->name, + 'slug' => 'external' === $type ? $category['slug'] : $category->slug, + ); + } + } + + /** + * Filter the categories that should be available for pull. + * + * @param array $categories Categories available for pull with name and slug. + * @param array $remote_categories Categories available from the remote connection. + * @param array $local_categories Categories registered on the local site. + * @param Connection $connection Distributor connection object. + * @param string $type Distributor connection type. + * + * @return array Categories available for pull with name and slug. + */ + $pull_categories = apply_filters( 'dt_available_pull_categories', $categories, $remote_categories, $local_categories, $connection, $type ); + + if ( ! empty( $pull_categories ) ) { + $categories = array(); + foreach ( $pull_categories as $category ) { + if ( in_array( $category['slug'], $distributable_categories, true ) ) { + $categories[] = $category; + } + } + } + + return $categories; +} + /** * Return post types that are allowed to be distributed * @@ -332,6 +390,35 @@ function distributable_post_types( $output = 'names' ) { return $post_types; } +/** + * Return categories that are allowed to be distributed + * + * @param string $output Optional. The type of output to return. + * Accepts category 'names' or 'objects'. Default 'names'. + * + * @return array + */ +function distributable_categories( $categories = [] ) { + if ( empty( $categories ) ) { + $categories = get_categories( [ 'hide_empty' => false ] ); + } else { + $categories = wp_list_pluck( $categories, 'slug' ); + } + + /** + * Filter categories that are distributable. + * + * @hook distributable_categories + * + * @param {array} $categories Categories that are distributable. + * + * @return {array} Categories that are distributable. + */ + $categories = apply_filters( 'distributable_categories', $categories ); + + return $categories; +} + /** * Return post statuses that are allowed to be distributed. *