From e41fd700930885b1e85b0524dd2d42f466782524 Mon Sep 17 00:00:00 2001 From: Sanketio Date: Tue, 12 Aug 2025 21:35:39 +0530 Subject: [PATCH 1/5] Allow taxonomy filter with remote site posts when pulling content --- assets/css/admin-pull-table.scss | 15 ++ assets/js/admin-pull.js | 85 ++++++- includes/classes/Connection.php | 16 ++ .../WordPressExternalConnection.php | 138 +++++++++++ .../NetworkSiteConnection.php | 56 +++++ includes/classes/PullListTable.php | 70 +++++- includes/pull-ui.php | 24 ++ includes/rest-api.php | 43 ++++ includes/utils.php | 230 +++++++++++++++++- 9 files changed, 671 insertions(+), 6 deletions(-) diff --git a/assets/css/admin-pull-table.scss b/assets/css/admin-pull-table.scss index a5d37a417..bb6ee71df 100644 --- a/assets/css/admin-pull-table.scss +++ b/assets/css/admin-pull-table.scss @@ -18,6 +18,21 @@ padding-left: 10px; } } + + .pull-taxonomy { + + &.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..60cfa0c89 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,41 @@ 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 ) => { + 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' ); + } + } ); + } else { + pullTaxonomies.forEach( ( taxonomyField ) => { + jQuery( taxonomyField ).addClass( 'hide' ); + jQuery( taxonomyField ).removeClass( 'show' ); + } ); + } + } + } + } ); + if ( choosePostTypeBtn ) { jQuery( choosePostTypeBtn ).on( 'click', ( event ) => { event.preventDefault(); @@ -49,6 +86,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 +165,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 }=${ + 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 +196,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 12d5742d2..c7f90f40c 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 ce770828d..1a72aaea2 100644 --- a/includes/classes/PullListTable.php +++ b/includes/classes/PullListTable.php @@ -480,6 +480,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 { @@ -626,6 +643,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,6 +659,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 ) : ?> @@ -644,13 +678,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 e7d026a94..eaa3e3c43 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. * From bd41e0a7cd9e6cd739997e0a79ade59d0da8e9a2 Mon Sep 17 00:00:00 2001 From: Sanket Parmar Date: Tue, 12 Aug 2025 21:39:40 +0530 Subject: [PATCH 2/5] Potential fix for code scanning alert no. 3: DOM text reinterpreted as HTML Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- assets/js/admin-pull.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/js/admin-pull.js b/assets/js/admin-pull.js index 60cfa0c89..c142f9e86 100755 --- a/assets/js/admin-pull.js +++ b/assets/js/admin-pull.js @@ -172,7 +172,9 @@ const getURL = () => { pullTaxonomies.forEach( ( taxonomyField ) => { if ( jQuery( taxonomyField ).hasClass( 'show' ) ) { taxonomies += `${ taxonomyField.id }=${ - taxonomyField.options[ taxonomyField.selectedIndex ].value + escapeURLComponent( + taxonomyField.options[ taxonomyField.selectedIndex ].value + ) }&`; } } ); From 0947ec3ee6f76974661eea2e22f39c0c4cb28444 Mon Sep 17 00:00:00 2001 From: Sanketio Date: Tue, 12 Aug 2025 23:08:34 +0530 Subject: [PATCH 3/5] Dynamically add columns to the list table for the supported taxonomies --- assets/js/admin-pull.js | 8 ++-- includes/classes/PullListTable.php | 61 ++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/assets/js/admin-pull.js b/assets/js/admin-pull.js index c142f9e86..12bb16392 100755 --- a/assets/js/admin-pull.js +++ b/assets/js/admin-pull.js @@ -171,11 +171,9 @@ const getURL = () => { if ( pullTaxonomies ) { pullTaxonomies.forEach( ( taxonomyField ) => { if ( jQuery( taxonomyField ).hasClass( 'show' ) ) { - taxonomies += `${ taxonomyField.id }=${ - escapeURLComponent( - taxonomyField.options[ taxonomyField.selectedIndex ].value - ) - }&`; + taxonomies += `${ taxonomyField.id }=${ escapeURLComponent( + taxonomyField.options[ taxonomyField.selectedIndex ].value + ) }&`; } } ); } diff --git a/includes/classes/PullListTable.php b/includes/classes/PullListTable.php index 1a72aaea2..c3dae721c 100644 --- a/includes/classes/PullListTable.php +++ b/includes/classes/PullListTable.php @@ -55,14 +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' ), - 'categories' => esc_html__( 'Categories', 'distributor' ), - 'date' => esc_html__( 'Date', 'distributor' ), + 'cb' => '', + 'name' => esc_html__( 'Name', 'distributor' ), + 'post_type' => esc_html__( 'Post Type', '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. * @@ -250,22 +266,6 @@ public function column_date( $post ) { } } - /** - * Output categories column. - * - * @param \WP_Post $post Post object. - * @since 2.0.5 - */ - public function column_categories( $post ) { - $categories = $post->terms['category'] ?? []; - - if ( empty( $categories ) ) { - return; - } - - echo wp_kses_post( generate_taxonomy_links( 'category', $post, $categories ) ); - } - /** * Output standard table columns. * @@ -276,6 +276,9 @@ public function column_categories( $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 ); @@ -284,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. * From 49f4a33e17f4c85d2176374000caad27261f1fbd Mon Sep 17 00:00:00 2001 From: Sanketio Date: Wed, 13 Aug 2025 20:02:13 +0530 Subject: [PATCH 4/5] Toggle reset button when post type changes --- assets/css/admin-pull-table.scss | 3 ++- assets/js/admin-pull.js | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/assets/css/admin-pull-table.scss b/assets/css/admin-pull-table.scss index bb6ee71df..c4e968051 100644 --- a/assets/css/admin-pull-table.scss +++ b/assets/css/admin-pull-table.scss @@ -19,7 +19,8 @@ } } - .pull-taxonomy { + .pull-taxonomy, + .dt-reset-filters-button { &.hide { display: none; diff --git a/assets/js/admin-pull.js b/assets/js/admin-pull.js index 12bb16392..b0c5d5231 100755 --- a/assets/js/admin-pull.js +++ b/assets/js/admin-pull.js @@ -45,6 +45,7 @@ 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 ) { @@ -65,15 +66,35 @@ if ( chooseConnection && choosePostType && form ) { 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 ) { From ffbafbc63bbd72ae86e6fbb4fb54f8878ce9d4ce Mon Sep 17 00:00:00 2001 From: Sanketio Date: Thu, 21 Aug 2025 18:37:38 +0530 Subject: [PATCH 5/5] Open taxonomy terms from the remote site in a new tab --- includes/utils.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/includes/utils.php b/includes/utils.php index eaa3e3c43..6709e402c 100644 --- a/includes/utils.php +++ b/includes/utils.php @@ -786,7 +786,7 @@ function generate_taxonomy_links( $taxonomy, $post, $terms = [] ) { $label = esc_html( sanitize_term_field( 'name', $t->name, $t->term_id, $taxonomy, 'display' ) ); - $term_links[] = get_edit_link( $posts_in_term_qv, $label ); + $term_links[] = get_edit_link( $posts_in_term_qv, $label, '', true ); } /** @@ -816,13 +816,14 @@ function generate_taxonomy_links( $taxonomy, $post, $terms = [] ) { * * @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 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 = '' ) { +function get_edit_link( $args, $link_text, $css_class = '', $should_open_in_new_tab = false ) { global $connection_now; @@ -842,6 +843,7 @@ function get_edit_link( $args, $link_text, $css_class = '' ) { $class_html = ''; $aria_current = ''; + $target = ''; if ( ! empty( $css_class ) ) { $class_html = sprintf( @@ -854,11 +856,16 @@ function get_edit_link( $args, $link_text, $css_class = '' ) { } } + if ( $should_open_in_new_tab ) { + $target = ' target="_blank"'; + } + return sprintf( - '%s', + '%s', esc_url( $url ), $class_html, $aria_current, + $target, $link_text ); }