diff --git a/assets/css/admin-pull-table.scss b/assets/css/admin-pull-table.scss index a5d37a417..c4e968051 100644 --- a/assets/css/admin-pull-table.scss +++ b/assets/css/admin-pull-table.scss @@ -18,6 +18,22 @@ padding-left: 10px; } } + + .pull-taxonomy, + .dt-reset-filters-button { + + &.hide { + display: none; + } + + &.show { + display: block; + } + } + + .dt-reset-filters-button { + margin-left: 6px + } } .wp-list-table .disabled { diff --git a/assets/js/admin-pull.js b/assets/js/admin-pull.js index 4488187a4..b0c5d5231 100755 --- a/assets/js/admin-pull.js +++ b/assets/js/admin-pull.js @@ -22,11 +22,13 @@ 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 choosePostTypeReset = document.getElementById( 'pull_post_type_reset' ); const searchField = document.getElementById( 'post-search-input' ); const searchBtn = document.getElementById( 'search-submit' ); const form = document.getElementById( 'posts-filter' ); const asDraftCheckboxes = document.querySelectorAll( '[name=dt_as_draft]' ); const pullLinks = document.querySelectorAll( '.distributor_page_pull .pull a' ); +const pullTaxonomies = document.querySelectorAll( '.pull-taxonomy' ); jQuery( chooseConnection ).on( 'change', ( event ) => { const pullUrlId = @@ -39,6 +41,62 @@ jQuery( chooseConnection ).on( 'change', ( event ) => { } ); if ( chooseConnection && choosePostType && form ) { + /** + * When the post type is changed, show/hide the taxonomy fields based on the post type. + */ + jQuery( choosePostType ).on( 'change', ( event ) => { + let shouldHideResetFiltersButton = false; + const selectedPostType = + event.currentTarget.options[ event.currentTarget.selectedIndex ]; + if ( selectedPostType ) { + const dataTaxonomies = + selectedPostType.getAttribute( 'data-taxonomies' ); + if ( dataTaxonomies ) { + const supportedTaxonomies = JSON.parse( dataTaxonomies ); + if ( supportedTaxonomies.length > 0 ) { + pullTaxonomies.forEach( ( taxonomyField ) => { + if ( + supportedTaxonomies.includes( + taxonomyField.id.replace( 'pull_', '' ) + ) + ) { + jQuery( taxonomyField ).addClass( 'show' ); + jQuery( taxonomyField ).removeClass( 'hide' ); + } else { + jQuery( taxonomyField ).addClass( 'hide' ); + jQuery( taxonomyField ).removeClass( 'show' ); + } + + if ( + ! shouldHideResetFiltersButton && + 'all' !== + taxonomyField.options[ + taxonomyField.selectedIndex + ].value + ) { + shouldHideResetFiltersButton = true; + } + } ); + } else { + pullTaxonomies.forEach( ( taxonomyField ) => { + jQuery( taxonomyField ).addClass( 'hide' ); + jQuery( taxonomyField ).removeClass( 'show' ); + } ); + + shouldHideResetFiltersButton = true; + } + } + } + + if ( shouldHideResetFiltersButton ) { + jQuery( choosePostTypeReset ).addClass( 'hide' ); + jQuery( choosePostTypeReset ).removeClass( 'show' ); + } else { + jQuery( choosePostTypeReset ).addClass( 'show' ); + jQuery( choosePostTypeReset ).removeClass( 'hide' ); + } + } ); + if ( choosePostTypeBtn ) { jQuery( choosePostTypeBtn ).on( 'click', ( event ) => { event.preventDefault(); @@ -49,6 +107,35 @@ if ( chooseConnection && choosePostType && form ) { } ); } + /** + * When the reset filters button is clicked, reset the filters and reload the page. + */ + if ( choosePostTypeReset ) { + jQuery( choosePostTypeReset ).on( 'click', ( event ) => { + event.preventDefault(); + + const pullUrlId = escapeURLComponent( + chooseConnection.options[ + chooseConnection.selectedIndex + ].getAttribute( 'data-pull-url-id' ) + ); + + const baseURL = getPullUrl( pullUrlId ); + let status = 'new'; + + if ( -1 < ` ${ form.className } `.indexOf( ' status-skipped ' ) ) { + status = 'skipped'; + } else if ( + -1 < ` ${ form.className } `.indexOf( ' status-pulled ' ) + ) { + status = 'pulled'; + } + + document.location = `${ baseURL }&status=${ status }`; + document.body.className += ' ' + 'dt-loading'; + } ); + } + if ( searchField && searchBtn ) { jQuery( searchBtn ).on( 'click', ( event ) => { event.preventDefault(); @@ -99,6 +186,23 @@ const getURL = () => { const postType = escapeURLComponent( choosePostType.options[ choosePostType.selectedIndex ].value ); + + // Build the taxonomies query string. + let taxonomies = ''; + if ( pullTaxonomies ) { + pullTaxonomies.forEach( ( taxonomyField ) => { + if ( jQuery( taxonomyField ).hasClass( 'show' ) ) { + taxonomies += `${ taxonomyField.id }=${ escapeURLComponent( + taxonomyField.options[ taxonomyField.selectedIndex ].value + ) }&`; + } + } ); + } + + if ( taxonomies ) { + taxonomies = taxonomies.slice( 0, -1 ); + } + const pullUrlId = escapeURLComponent( chooseConnection.options[ chooseConnection.selectedIndex ].getAttribute( 'data-pull-url-id' @@ -113,5 +217,5 @@ const getURL = () => { status = 'pulled'; } - return `${ baseURL }&pull_post_type=${ postType }&status=${ status }`; + return `${ baseURL }&pull_post_type=${ postType }&status=${ status }&${ taxonomies }`; }; diff --git a/includes/classes/Connection.php b/includes/classes/Connection.php index 9e22f5441..00e1e12f2 100644 --- a/includes/classes/Connection.php +++ b/includes/classes/Connection.php @@ -65,6 +65,22 @@ abstract public function get_sync_log( $id ); */ abstract public function get_post_types(); + /** + * Get available post type taxonomies from a connection + * + * @param string $post_type Post type. + * + * @return array + */ + abstract public function get_post_type_taxonomies( $post_type ); + + /** + * Get available taxonomy terms from a connection + * + * @return array + */ + abstract public function get_taxonomy_terms(); + /** * 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..699412155 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 taxonomy term to pull. + * + * @var string + */ + public $pull_taxonomy_term; + + /** + * Default taxonomy terms to show in filter. + * + * @var string + */ + public $pull_taxonomy_terms; + /** * This is a utility function for parsing annoying API link headers returned by the types endpoint * @@ -168,6 +182,11 @@ public function remote_get( $args = array() ) { } } + // Add the tax query to the query args. + if ( isset( $args['tax_query'] ) ) { + $query_args['tax_query'] = $args['tax_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_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 +701,125 @@ public function get_post_types() { return $types_body_array; } + /** + * Get the available post type taxonomies. + * The taxonomies with external connection are already available in the post type object. + * + * @param string $post_type Post type. + * + * @return array + */ + public function get_post_type_taxonomies( $post_type ) { + return array(); + } + + /** + * Get the available taxonomies from the remote connection. + * + * @param array $taxonomies Taxonomies to get. + * + * @return array|\WP_Error Array of taxonomies with rest_base and label, or WP_Error if the request fails. + */ + private function get_remote_taxonomies( $taxonomies = array() ) { + + $path = self::$namespace; + + $taxonomies_path = untrailingslashit( $this->base_url ) . '/' . $path . '/taxonomies'; + + $taxonomies_response = Utils\remote_http_request( + $taxonomies_path, + $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) + ); + + if ( is_wp_error( $taxonomies_response ) ) { + return $taxonomies_response; + } + + if ( 404 === wp_remote_retrieve_response_code( $taxonomies_response ) ) { + return new \WP_Error( 'bad-endpoint', esc_html__( 'Could not connect to API endpoint.', 'distributor' ) ); + } + + $taxonomies_body = wp_remote_retrieve_body( $taxonomies_response ); + + if ( empty( $taxonomies_body ) ) { + return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty.', 'distributor' ) ); + } + + $taxonomies_body_array = json_decode( $taxonomies_body, true ); + + $taxonomies_exists = array(); + foreach ( $taxonomies as $taxonomy ) { + if ( isset( $taxonomies_body_array[ $taxonomy ] ) ) { + $taxonomies_exists[ $taxonomy ] = array( + 'rest_base' => $taxonomies_body_array[ $taxonomy ]['rest_base'], + 'label' => $taxonomies_body_array[ $taxonomy ]['name'], + ); + } + } + + if ( empty( $taxonomies_exists ) ) { + return new \WP_Error( 'no-taxonomies', esc_html__( 'No taxonomies found.', 'distributor' ) ); + } + + return $taxonomies_exists; + } + + /** + * Get the available taxonomy terms. + * + * @param array $taxonomies Taxonomies to get terms for. + * + * @return array|\WP_Error Array of taxonomy terms with items and label, or WP_Error if the request fails. + */ + public function get_taxonomy_terms( $taxonomies = array() ) { + + // Get the remote taxonomies, if the request fails, return an empty array. + $remote_taxonomies = $this->get_remote_taxonomies( $taxonomies ); + if ( empty( $remote_taxonomies ) || is_wp_error( $remote_taxonomies ) ) { + return array(); + } + + $path = self::$namespace; + + $taxonomy_terms = array(); + + /** + * Loop through the remote taxonomies and get the terms for each taxonomy. + */ + foreach ( $remote_taxonomies as $taxonomy => $taxonomy_data ) { + + $taxonomy_path = untrailingslashit( $this->base_url ) . '/' . $path . '/' . $taxonomy_data['rest_base']; + + $taxonomy_response = Utils\remote_http_request( + $taxonomy_path, + $this->auth_handler->format_get_args( array( 'timeout' => self::$timeout ) ) + ); + + if ( is_wp_error( $taxonomy_response ) ) { + continue; + } + + if ( 404 === wp_remote_retrieve_response_code( $taxonomy_response ) ) { + continue; + } + + $taxonomy_body = wp_remote_retrieve_body( $taxonomy_response ); + + if ( empty( $taxonomy_body ) ) { + continue; + } + + $taxonomy_body_array = json_decode( $taxonomy_body, true ); + + $taxonomy_terms[ $taxonomy ] = array( + 'items' => $taxonomy_body_array, + 'label' => $taxonomy_data['label'], + ); + } + + return $taxonomy_terms; + } + /** * 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 e2bbd9dbc..33ed3d409 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 taxonomy term to pull. + * + * @var string + */ + public $pull_taxonomy_term; + + /** + * Default taxonomy terms to show in filter. + * + * @var string + */ + public $pull_taxonomy_terms; + /** * Set up network site connection * @@ -520,6 +534,43 @@ public function get_post_types() { return $post_types; } + /** + * Get the available post type taxonomies. + * + * @param string $post_type Post type. + * + * @return array + */ + public function get_post_type_taxonomies( $post_type ) { + switch_to_blog( $this->site->blog_id ); + + $post_type_taxonomies = array(); + + $object_taxonomies = get_object_taxonomies( $post_type, 'objects' ); + foreach ( $object_taxonomies as $taxonomy_name => $taxonomy_object ) { + $post_type_taxonomies[ $taxonomy_name ] = $taxonomy_object->label; + } + + restore_current_blog(); + + return $post_type_taxonomies; + } + + /** + * Get the available taxonomy terms. + * + * @param array $taxonomies Taxonomies to get terms for. + * + * @return array|\WP_Error Array of taxonomy terms with items and label, or WP_Error if the request fails. + */ + public function get_taxonomy_terms( $taxonomies = array() ) { + switch_to_blog( $this->site->blog_id ); + $taxonomy_terms = Utils\distributable_taxonomy_terms( $taxonomies ); + restore_current_blog(); + + return $taxonomy_terms; + } + /** * Remotely get posts so we can list them for pulling * @@ -567,6 +618,11 @@ public function remote_get( $args = array(), $new_post_args = array() ) { $query_args['post__not_in'] = $args['post__not_in']; } + // Add the tax query to the query args. + if ( isset( $args['tax_query'] ) ) { + $query_args['tax_query'] = $args['tax_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_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 8c1041a88..c3dae721c 100644 --- a/includes/classes/PullListTable.php +++ b/includes/classes/PullListTable.php @@ -7,6 +7,8 @@ namespace Distributor; +use function Distributor\Utils\generate_taxonomy_links; + /** * List table class for pull screen */ @@ -53,13 +55,30 @@ public function __construct() { * @return array */ public function get_columns() { + + global $connection_now; + $columns = [ 'cb' => '', 'name' => esc_html__( 'Name', 'distributor' ), 'post_type' => esc_html__( 'Post Type', 'distributor' ), - 'date' => esc_html__( 'Date', 'distributor' ), ]; + /** + * Dynamically add the taxonomies to the columns, only if the post type supports the taxonomy. + */ + if ( ! empty( $connection_now->pull_taxonomy_terms ) ) { + + foreach ( $connection_now->pull_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + if ( ! empty( $taxonomy_data['post_types'] ) && in_array( $connection_now->pull_post_type, $taxonomy_data['post_types'], true ) ) { + $columns[ $taxonomy ] = $taxonomy_data['label']; + } + } + } + + $columns['date'] = esc_html__( 'Date', 'distributor' ); + /** * Filters the columns displayed in the pull list table. * @@ -257,6 +276,9 @@ public function column_date( $post ) { * @since 0.8 */ public function column_default( $item, $column_name ) { + + global $connection_now; + if ( 'post_type' === $column_name ) { $post_type = get_post_type_object( $item->post_type ); @@ -265,6 +287,22 @@ public function column_default( $item, $column_name ) { } } + // If the post type supports the taxonomy, output the taxonomy links. + if ( ! empty( $connection_now->pull_taxonomy_terms ) ) { + + foreach ( $connection_now->pull_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + // If the post type does not support the taxonomy, skip it. + if ( empty( $taxonomy_data['post_types'] ) || ! in_array( $connection_now->pull_post_type, $taxonomy_data['post_types'], true ) ) { + continue; + } + + if ( $column_name === $taxonomy ) { + return wp_kses_post( generate_taxonomy_links( $taxonomy, $item, $item->terms[ $taxonomy ] ) ); + } + } + } + /** * Fires for each column in the pull list table. * @@ -461,6 +499,23 @@ public function prepare_items() { $remote_get_args['s'] = rawurlencode( $_GET['s'] ); // @codingStandardsIgnoreLine Nonce isn't required. } + // Add taxonomy filters to the remote get arguments. + if ( ! empty( $connection_now->pull_taxonomy_terms ) ) { + + foreach ( $connection_now->pull_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + if ( 'all' === $connection_now->pull_taxonomy_term[ $taxonomy ] ) { + continue; + } + + $remote_get_args['tax_query'][] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'taxonomy' => $taxonomy, + 'field' => 'slug', + 'terms' => $connection_now->pull_taxonomy_term[ $taxonomy ], + ); + } + } + if ( is_a( $connection_now, '\Distributor\ExternalConnection' ) ) { $this->sync_log = get_post_meta( $connection_now->id, 'dt_sync_log', true ); } else { @@ -607,6 +662,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' ) ) { @@ -615,6 +678,15 @@ public function extra_tablenav( $which ) { $connection_type = 'external'; } + // Check if there are any filters applied. + $has_filters = false; + foreach ( $connection_now->pull_taxonomy_term as $taxonomy => $selected_term ) { + if ( 'all' !== $selected_term ) { + $has_filters = true; + break; + } + } + if ( $connection_now && $connection_now->pull_post_types ) : ?> @@ -625,13 +697,47 @@ public function extra_tablenav( $which ) { pull_post_types as $post_type ) : ?> - + pull_taxonomy_terms ) ) : ?> + pull_taxonomy_terms as $taxonomy => $taxonomy_data ) : ?> + pull_post_type, $taxonomy_data['post_types'], true ) ) { + $toggle_class = 'hide'; + } + ?> + + + + + + + pull_post_types as $post_type ) { + $supported_taxonomies[ $post_type['slug'] ] = $post_type['taxonomies']; + } + + // Get the available taxonomy terms. + $connection_now->pull_taxonomy_term = []; + $connection_now->pull_taxonomy_terms = \Distributor\Utils\available_pull_taxonomy_terms( $connection_now, $connection_type, $supported_taxonomies ); + if ( ! empty( $connection_now->pull_taxonomy_terms ) ) { + + foreach ( $connection_now->pull_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + $term_slugs = wp_list_pluck( $taxonomy_data['items'], 'slug' ); + + // Set the taxonomy term to pull. + if ( isset( $_GET["pull_{$taxonomy}"] ) && in_array( $_GET["pull_{$taxonomy}"], $term_slugs, true ) ) { + $connection_now->pull_taxonomy_term[ $taxonomy ] = $_GET["pull_{$taxonomy}"]; + } else { + $connection_now->pull_taxonomy_term[ $taxonomy ] = 'all'; + } + } + } ?> diff --git a/includes/rest-api.php b/includes/rest-api.php index 8d60a4f7c..9ae60d639 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,11 @@ function get_pull_content_list( $request ) { $args['orderby'] = 'relevance'; } + // Add the tax query to the query args. + if ( isset( $request['tax_query'] ) ) { + $args['tax_query'] = $request['tax_query']; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_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 c89860658..b133c2d79 100644 --- a/includes/utils.php +++ b/includes/utils.php @@ -243,14 +243,51 @@ function available_pull_post_types( $connection, $type ) { return []; } - $local_post_types = array_diff_key( get_post_types( [ 'public' => true ], 'objects' ), array_flip( [ 'attachment', 'dt_ext_connection', 'dt_subscription' ] ) ); - $available_post_types = array_intersect_key( $remote_post_types, $local_post_types ); + // Get the local post types. + $local_post_types = array_diff_key( get_post_types( [ 'public' => true ], 'objects' ), array_flip( [ 'attachment', 'dt_ext_connection', 'dt_subscription' ] ) ); + + /** + * Loop through the remote post types and get the taxonomies for each post type. + * If the post type is not available on the local site, skip it. + */ + $available_post_types = array(); + foreach ( $remote_post_types as $post_type_slug => $post_type_data ) { + + // Skip if the post type is not available on the local site. + if ( empty( $local_post_types[ $post_type_slug ] ) ) { + continue; + } + + // Get the taxonomies for the post type. + if ( 'external' === $type ) { + $remote_post_type_taxonomies = $post_type_data['taxonomies']; + } else { + $remote_post_type_taxonomies = $connection->get_post_type_taxonomies( $post_type_slug ); + } + + // Update the post type data with the taxonomies. + $updated_post_type_data = $post_type_data; + + // If the post type has taxonomies, update the post type data with the taxonomies. + if ( ! empty( $remote_post_type_taxonomies ) ) { + + if ( 'external' === $type ) { + $updated_post_type_data['taxonomies'] = array_combine( $remote_post_type_taxonomies, $remote_post_type_taxonomies ); + } else { + $updated_post_type_data->taxonomies = $remote_post_type_taxonomies; + } + } + + // Add the post type data to the available post types array. + $available_post_types[ $post_type_slug ] = $updated_post_type_data; + } if ( ! empty( $available_post_types ) ) { foreach ( $available_post_types as $post_type ) { $post_types[] = array( - 'name' => 'external' === $type ? $post_type['name'] : $post_type->label, - 'slug' => 'external' === $type ? $post_type['slug'] : $post_type->name, + 'name' => 'external' === $type ? $post_type['name'] : $post_type->label, + 'slug' => 'external' === $type ? $post_type['slug'] : $post_type->name, + 'taxonomies' => 'external' === $type ? $post_type['taxonomies'] : $post_type->taxonomies, ); } } @@ -285,6 +322,128 @@ function available_pull_post_types( $connection, $type ) { return $post_types; } +/** + * Get the taxonomy terms that are available for pull. + * + * @param \Distributor\Connection $connection Connection object. + * @param string $type Connection type. + * @param array $supported_taxonomies Supported taxonomies. + * + * @return array Array of taxonomy terms. + */ +function available_pull_taxonomy_terms( $connection, $type, $supported_taxonomies ) { + + // Generate the taxonomy post type relation. + $taxonomy_post_type_relation = array(); + foreach ( $supported_taxonomies as $post_type => $taxonomies ) { + + foreach ( $taxonomies as $taxonomy_slug => $taxonomy_name ) { + + if ( isset( $taxonomy_post_type_relation[ $taxonomy_slug ] ) ) { + $taxonomy_post_type_relation[ $taxonomy_slug ][] = $post_type; + } else { + $taxonomy_post_type_relation[ $taxonomy_slug ] = array( $post_type ); + } + } + } + + /** + * Get the common supported taxonomies. + * Include all taxonomies from all post types. + */ + $common_supported_taxonomies = array(); + foreach ( $supported_taxonomies as $taxonomies ) { + $common_supported_taxonomies = array_merge( $common_supported_taxonomies, array_keys( $taxonomies ) ); + } + $common_supported_taxonomies = array_unique( $common_supported_taxonomies ); + + /** + * Filter the taxonomies that should be allowed to be pulled. + * + * @hook dt_allowed_pull_taxonomies + * + * @param {array} $allowed_taxonomies Array of allowed taxonomies. + * @param {array} $common_supported_taxonomies Array of common supported taxonomies from all post types. + * + * @return {array} Array of allowed taxonomies. + */ + $allowed_taxonomies = apply_filters( 'dt_allowed_pull_taxonomies', array( 'category' ), $common_supported_taxonomies ); + + // Return empty array, if no taxonomies are allowed to be pulled. + if ( empty( $allowed_taxonomies ) || ! is_array( $allowed_taxonomies ) ) { + return array(); + } + + // Remove taxonomies that are not supported by the remote site. + $allowed_taxonomies = array_intersect( $allowed_taxonomies, $common_supported_taxonomies ); + + // Get the taxonomy terms from the remote site. + $remote_taxonomy_terms = $connection->get_taxonomy_terms( $allowed_taxonomies ); + if ( empty( $remote_taxonomy_terms ) || is_wp_error( $remote_taxonomy_terms ) ) { + return array(); + } + + // Get the distributable taxonomy terms. + $distributable_taxonomy_terms = distributable_taxonomy_terms( $allowed_taxonomies, $remote_taxonomy_terms ); + + $taxonomy_terms = array(); + foreach ( $remote_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + $taxonomy_terms[ $taxonomy ]['label'] = empty( $taxonomy_data['label'] ) ? '' : $taxonomy_data['label']; + + foreach ( $taxonomy_data['items'] as $term ) { + $taxonomy_terms[ $taxonomy ]['items'][] = array( + 'name' => 'external' === $type ? $term['name'] : $term->name, + 'slug' => 'external' === $type ? $term['slug'] : $term->slug, + ); + } + } + + /** + * Filter the taxonomy terms that should be available for pull. + * + * @param array $taxonomy_terms Taxonomy terms available for pull with name and slug. + * @param array $remote_taxonomy_terms Taxonomy terms available from the remote connection. + * @param Connection $connection Distributor connection object. + * @param string $type Distributor connection type. + * + * @return array Categories available for pull with name and slug. + */ + $pull_taxonomy_terms = apply_filters( 'dt_available_pull_taxonomy_terms', $taxonomy_terms, $remote_taxonomy_terms, $connection, $type ); + if ( empty( $pull_taxonomy_terms ) || ! is_array( $pull_taxonomy_terms ) ) { + return array(); + } + + $final_taxonomy_terms = array(); + + /** + * Loop through the pull taxonomy terms and add the distributable terms to the final taxonomy terms array. + * If the taxonomy or term is not distributable, skip it. + */ + foreach ( $pull_taxonomy_terms as $taxonomy => $taxonomy_data ) { + + // Skip if the taxonomy is not distributable. + if ( ! isset( $distributable_taxonomy_terms[ $taxonomy ] ) || empty( $distributable_taxonomy_terms[ $taxonomy ]['items'] ) ) { + continue; + } + + // Add the taxonomy label. + $final_taxonomy_terms[ $taxonomy ]['label'] = empty( $taxonomy_data['label'] ) ? '' : $taxonomy_data['label']; + + // Add the post types that support the taxonomy. + $final_taxonomy_terms[ $taxonomy ]['post_types'] = empty( $taxonomy_post_type_relation[ $taxonomy ] ) ? array() : $taxonomy_post_type_relation[ $taxonomy ]; + + // Skip if the term is not distributable. + foreach ( $taxonomy_data['items'] as $term ) { + if ( in_array( $term['slug'], $distributable_taxonomy_terms[ $taxonomy ]['items'], true ) ) { + $final_taxonomy_terms[ $taxonomy ]['items'][] = $term; + } + } + } + + return $final_taxonomy_terms; +} + /** * Return post types that are allowed to be distributed * @@ -332,6 +491,69 @@ function distributable_post_types( $output = 'names' ) { return $post_types; } +/** + * Get the distributable taxonomy terms. + * Loop through the taxonomies and get the terms. + * Filter the terms to only include the distributable terms. + * Return the terms. + * + * @param array $taxonomies Array of taxonomies. + * @param array $terms Array of terms. + * + * @return array Array of distributable taxonomy terms. + */ +function distributable_taxonomy_terms( $taxonomies = array(), $terms = array() ) { + + // Return empty array, if no taxonomies are provided. + if ( empty( $taxonomies ) ) { + return array(); + } + + $found_all_terms = array(); + + foreach ( $taxonomies as $taxonomy ) { + + // Get the terms, if the terms are not provided. + if ( empty( $terms[ $taxonomy ]['items'] ) ) { + $found_terms = get_terms( + array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + ) + ); + } else { + $found_terms = wp_list_pluck( $terms[ $taxonomy ]['items'], 'slug' ); + } + + /** + * Filter the taxonomy terms that should be distributable. + * + * @hook distributable_{$taxonomy}_terms + * + * @param {array} $terms Array of terms. + * @param {string} $taxonomy Taxonomy name. + * + * @return {array} Array of terms. + */ + $found_terms = apply_filters( "distributable_{$taxonomy}_terms", $found_terms, $taxonomy ); + + // Skip if the terms are empty, a WP_Error, or not an array. + if ( empty( $found_terms ) || is_wp_error( $found_terms ) || ! is_array( $found_terms ) ) { + continue; + } + + // Get the taxonomy label. + $taxonomy_label = empty( $terms[ $taxonomy ]['label'] ) ? get_taxonomy( $taxonomy )->labels->name : $terms[ $taxonomy ]['label']; + + $found_all_terms[ $taxonomy ] = array( + 'items' => $found_terms, + 'label' => $taxonomy_label, + ); + } + + return $found_all_terms; +} + /** * Return post statuses that are allowed to be distributed. * @@ -491,6 +713,163 @@ function prepare_meta( $post_id ) { return $prepared_meta; } +/** + * Generates taxonomy term links for a given post. + * + * The code is taken from WP_Posts_List_Table::column_default and modified + * lightly to work in our context. + * + * @param string $taxonomy The taxonomy name. + * @param object $post The post object. + * @param array $terms Optional. Array of terms. + * + * @return string The generated HTML for the taxonomy links. + */ +function generate_taxonomy_links( $taxonomy, $post, $terms = [] ) { + $taxonomy_object = get_taxonomy( $taxonomy ); + + if ( ! $taxonomy_object ) { + return ''; + } + + if ( ! $terms ) { + $terms = get_the_terms( $post, $taxonomy ); + } + + /** + * Filter the taxonomy terms that should be synced. + * + * @since 2.0.5 + * @hook dt_syncable_taxonomy_terms + * + * @param {array} $terms Array of terms. + * @param {string} $taxonomy Taxonomy name. + * @param {object} $post Post Object. + * + * @return {array} Array of terms. + */ + $terms = apply_filters( "dt_syncable_{$taxonomy}_terms", $terms, $taxonomy, $post ); + + /** + * Filter the terms that should be synced. + * + * @since 2.0.5 + * @hook dt_syncable_terms + * + * @param {array} $terms Array of categories. + * @param {string} $taxonomy Taxonomy name. + * @param {object} $post Post Object. + * + * @return {array} Array of categories. + */ + $terms = apply_filters( 'dt_syncable_terms', $terms, $taxonomy, $post ); + + if ( is_array( $terms ) ) { + $term_links = array(); + + foreach ( $terms as $t ) { + if ( is_array( $t ) ) { + $t = (object) $t; + } + $posts_in_term_qv = array(); + + if ( 'post' !== $post->post_type ) { + $posts_in_term_qv['post_type'] = $post->post_type; + } + + if ( $taxonomy_object->query_var ) { + $posts_in_term_qv[ $taxonomy_object->query_var ] = $t->slug; + } else { + $posts_in_term_qv['taxonomy'] = $taxonomy; + $posts_in_term_qv['term'] = $t->slug; + } + + $label = esc_html( sanitize_term_field( 'name', $t->name, $t->term_id, $taxonomy, 'display' ) ); + + $term_links[] = get_edit_link( $posts_in_term_qv, $label, '', true ); + } + + /** + * Filters the links in `$taxonomy` column of edit.php. + * + * @since 2.0.5 + * @hook dt_taxonomy_links + * + * @param {string[]} $term_links Array of term editing links. + * @param {string} $taxonomy Taxonomy name. + * @param {WP_Term[]} $terms Array of term objects appearing in the post row. + * + * @return {string[]} Array of term editing links. + */ + $term_links = apply_filters( 'dt_taxonomy_links', $term_links, $taxonomy, $terms ); + + return implode( wp_get_list_item_separator(), $term_links ); + } else { + return '' . $taxonomy_object->labels->no_terms . ''; + } +} + +/** + * Creates a link to edit.php with params. + * + * The edit link is created in such a way that it will link to source site. + * + * @since 2.0.5 + * + * @param string[] $args Associative array of URL parameters for the link. + * @param string $link_text Link text. + * @param string $css_class Optional. Class attribute. Default empty string. + * @param bool $should_open_in_new_tab Optional. Whether to open the link in a new tab. Default false. + * + * @return string The formatted link string. + */ +function get_edit_link( $args, $link_text, $css_class = '', $should_open_in_new_tab = false ) { + + global $connection_now; + + // Get the admin URL for the current connection. + $url = ''; + if ( is_a( $connection_now, '\Distributor\InternalConnections\NetworkSiteConnection' ) ) { + $url = add_query_arg( $args, get_admin_url( $connection_now->site->blog_id, 'edit.php' ) ); + } elseif ( is_a( $connection_now, '\Distributor\ExternalConnection' ) && ! empty( $connection_now->base_url ) ) { + $base_url = str_replace( '/wp-json', '', $connection_now->base_url ); + $url = add_query_arg( $args, trailingslashit( $base_url ) . 'wp-admin/edit.php' ); + } + + // If the URL is empty, return an empty string. + if ( empty( $url ) ) { + return ''; + } + + $class_html = ''; + $aria_current = ''; + $target = ''; + + if ( ! empty( $css_class ) ) { + $class_html = sprintf( + ' class="%s"', + esc_attr( $css_class ) + ); + + if ( 'current' === $css_class ) { + $aria_current = ' aria-current="page"'; + } + } + + if ( $should_open_in_new_tab ) { + $target = ' target="_blank"'; + } + + return sprintf( + '%s', + esc_url( $url ), + $class_html, + $aria_current, + $target, + $link_text + ); +} + /** * Format media items for consumption * diff --git a/tests/php/includes/common.php b/tests/php/includes/common.php index ea3e10088..225ebcda7 100644 --- a/tests/php/includes/common.php +++ b/tests/php/includes/common.php @@ -358,6 +358,10 @@ public function check_connections() { } public function remote_get( $args ) { } public function get_post_types() { } + + public function get_post_type_taxonomies( $post_type ) { } + + public function get_taxonomy_terms() { } } class TestInternalConnection extends \Distributor\Connection { @@ -374,4 +378,8 @@ public function log_sync( array $item_id_mappings, $id, $overwrite ) {} public function get_sync_log( $id ) {} public function get_post_types() { } + + public function get_post_type_taxonomies( $post_type ) { } + + public function get_taxonomy_terms() { } }