Published : August 8, 2026

CVE-2026-61959: Business Directory Plugin – Easy Listing Directories for WordPress <= 6.4.24 Authenticated (Subscriber+) Stored Cross-Site Scripting PoC, Patch Analysis & Rule

Severity Medium (CVSS 6.4)
CWE 79
Vulnerable Version 6.4.24
Patched Version 6.4.25
Disclosed July 27, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-61959: This vulnerability is a Stored Cross-Site Scripting (XSS) flaw in the Business Directory Plugin – Easy Listing Directories for WordPress, affecting versions up to and including 6.4.24. It allows authenticated attackers with subscriber-level access or higher to inject arbitrary web scripts that execute whenever a user accesses an affected page. The CVSS score is 6.4, indicating a medium severity threat.

Root Cause: The root cause lies in the insufficient input sanitization and output escaping within the plugin’s handling of custom radio button fields. Specifically, the `WPBDP_FieldTypes_RadioButton` class in the file `includes/fields/class-fieldtypes-radiobutton.php` did not properly validate or escape the values submitted for custom meta fields. Before the patch, the `convert_input` function passed any submitted input directly to the storage mechanism, and the `render_field_inner` function output the value without proper escaping. Furthermore, the `get_field_html_value` function lacked a custom implementation for meta fields, falling back to a parent method that did not escape the radio button’s stored label, which could be attacker-controlled. This allowed an attacker to submit a crafted value containing malicious JavaScript as the radio button’s option label or by manipulating the submitted field value.

Exploitation: An authenticated attacker with subscriber-level access can submit a listing form that includes a custom radio button field. By intercepting the submission request (e.g., to `/wp-admin/admin-ajax.php` or the listing submission endpoint), the attacker can modify the value of the `listingfields[FIELD_ID]` parameter to include a malicious XSS payload such as `alert(document.cookie)`. Because the plugin lacks proper output escaping, this payload is stored in the database. When an administrator or another user views the listing page, the stored script executes in their browser, potentially allowing the attacker to steal session cookies, perform actions on behalf of the administrator, or deface the site.

Patch Analysis: The patch introduces several key changes. In `convert_input`, the plugin now validates the input against a whitelist of stored options for meta fields, rejecting any values that do not match. It also adds a `get_field_html_value` method that applies `esc_html()` to the value for meta fields before output. Additionally, `render_field_inner` now uses `esc_attr()` for the option value and `esc_html()` for the label. By validating input against the known options and escaping all output, the patch prevents both the storage of malicious scripts and their execution when rendered, effectively closing the XSS vector.

Impact: Successful exploitation of this vulnerability allows an authenticated attacker to inject and execute arbitrary JavaScript in the context of an administrator’s session. This could lead to full site compromise if the attacker creates a rogue administrator account, modifies site content to inject malware or phishing links, or steals sensitive data like administrator session cookies. The attacker can also target other legitimate users to redirect them to malicious sites or exfiltrate their personal data.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/business-directory-plugin/business-directory-plugin.php
+++ b/business-directory-plugin/business-directory-plugin.php
@@ -3,7 +3,7 @@
  * Plugin Name: Business Directory Plugin
  * Plugin URI: https://businessdirectoryplugin.com
  * Description: Provides the ability to maintain a free or paid business directory on your WordPress powered site.
- * Version: 6.4.24
+ * Version: 6.4.25
  * Author: Business Directory Team
  * Author URI: https://businessdirectoryplugin.com
  * Text Domain: business-directory-plugin
--- a/business-directory-plugin/includes/admin/class-admin.php
+++ b/business-directory-plugin/includes/admin/class-admin.php
@@ -292,7 +292,8 @@
 			$response = wp_remote_post(
 				'https://feedback.strategy11.com/wp-json/frm/v2/entries',
 				array(
-					'body' => array(
+					'user-agent' => wpbdp_http_user_agent(),
+					'body'       => array(
 						'bd-firstname1' => $current_user->first_name,
 						'bd-email-1'    => $email,
 						'form_id'       => 'bd-plugin-course',
--- a/business-directory-plugin/includes/admin/controllers/class-onboarding-wizard.php
+++ b/business-directory-plugin/includes/admin/controllers/class-onboarding-wizard.php
@@ -270,7 +270,8 @@
 		wp_remote_post(
 			'https://feedback.strategy11.com/wp-json/frm/v2/entries',
 			array(
-				'body' => array(
+				'user-agent' => wpbdp_http_user_agent(),
+				'body'       => array(
 					'bd-firstname1' => $user->first_name,
 					'bd-email-1'    => $user->user_email,
 					'form_id'       => 'bd-plugin-course',
--- a/business-directory-plugin/includes/admin/controllers/class-themes-admin.php
+++ b/business-directory-plugin/includes/admin/controllers/class-themes-admin.php
@@ -420,8 +420,9 @@
 				WPBDP_Licensing::STORE_URL
 			),
 			array(
-				'timeout'   => 15,
-				'sslverify' => false,
+				'timeout'    => 15,
+				'user-agent' => wpbdp_http_user_agent(),
+				'sslverify'  => false,
 			)
 		);

@@ -532,7 +533,10 @@
 	 * @return false|string|WP_Error File path on success, WP_Error on download failure, false if not a valid ZIP.
 	 */
 	private function download_theme_zip( $url ) {
+		// phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args
+		add_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10, 2 );
 		$tmpfile = download_url( $url );
+		remove_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10 );

 		if ( is_wp_error( $tmpfile ) ) {
 			return $tmpfile;
--- a/business-directory-plugin/includes/admin/helpers/class-modules-api.php
+++ b/business-directory-plugin/includes/admin/helpers/class-modules-api.php
@@ -89,14 +89,11 @@

 		$this->set_running();

-		// We need to know the version number to allow different downloads.
-		$agent = 'Business Directory/' . WPBDP_VERSION;
-
 		$response = wp_remote_get(
 			$url,
 			array(
 				'timeout'    => 25,
-				'user-agent' => $agent . '; ' . get_bloginfo( 'url' ),
+				'user-agent' => wpbdp_http_user_agent(),
 			)
 		);

--- a/business-directory-plugin/includes/admin/upgrades/class-themes-updater.php
+++ b/business-directory-plugin/includes/admin/upgrades/class-themes-updater.php
@@ -96,8 +96,9 @@
 		$response = wp_remote_get(
 			add_query_arg( $request, 'http://businessdirectoryplugin.com/' ),
 			array(
-				'timeout'   => 15,
-				'sslverify' => false,
+				'timeout'    => 15,
+				'user-agent' => wpbdp_http_user_agent(),
+				'sslverify'  => false,
 			)
 		);

@@ -226,7 +227,10 @@
 			return new WP_Error( 'invalid_package_url', 'No package URL provided.' );
 		}

+		// phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args
+		add_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10, 2 );
 		$download_file = download_url( $url );
+		remove_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10 );
 		if ( is_wp_error( $download_file ) ) {
 			return new WP_Error( 'download_failed', 'Could not download theme package.', $download_file->get_error_message() );
 		}
--- a/business-directory-plugin/includes/admin/upgrades/migrations/migration-3_7.php
+++ b/business-directory-plugin/includes/admin/upgrades/migrations/migration-3_7.php
@@ -169,7 +169,7 @@

                             if ( ! $fee_info || ! term_exists( intval( $fee_info->category_id ), WPBDP_CATEGORY_TAX ) ) {
                                 $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->prefix}wpbdp_payments WHERE id = %d", $t['id'] ) );
-                                continue;
+                                continue 2;
                             }

                             $fee_info->fee = unserialize( $fee_info->fee );
--- a/business-directory-plugin/includes/class-query-integration.php
+++ b/business-directory-plugin/includes/class-query-integration.php
@@ -341,26 +341,16 @@
 	 * @return string listing ids
 	 */
 	private function get_sticky_listing_ids( $query ) {
-		global $wpdb;
-
-		$order_by  = $query->get( 'orderby' );
-		$order     = $query->get( 'order' );
-		$join_sort = in_array( $order_by, array( 'title', 'date', 'modified', 'author' ), true );
-
-		$query = "SELECT listing_id FROM {$wpdb->prefix}wpbdp_listings";
-		if ( $join_sort ) {
-			$query .= " JOIN {$wpdb->posts} p ON p.ID = {$wpdb->prefix}wpbdp_listings.listing_id";
-		}
-		$query .= ' WHERE is_sticky=1';
-		if ( $join_sort ) {
-			$query .= " ORDER BY p.post_{$order_by} {$order}";
-		}
+		$order_by  = (string) $query->get( 'orderby' );
+		$order     = strtoupper( (string) $query->get( 'order' ) );
+		$order     = in_array( $order, array( 'ASC', 'DESC' ), true ) ? $order : 'ASC';
+		$sql_query = $this->get_sticky_listing_ids_query( $order_by, $order );

 		$results = WPBDP_Utils::check_cache(
 			array(
-				'cache_key' => 'sticky_listing_idss',
+				'cache_key' => $this->get_sticky_listing_ids_cache_key( $order_by, $order ),
 				'group'     => 'wpbdp_listings',
-				'query'     => $query,
+				'query'     => $sql_query,
 				'type'      => 'get_col',
 			)
 		);
@@ -376,6 +366,79 @@
 		return implode( ',', $results );
 	}

+	/**
+	 * Get the SQL query for sticky listing ids.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $order_by The orderby value from the current query.
+	 * @param string $order    The normalized order direction.
+	 *
+	 * @return string
+	 */
+	private function get_sticky_listing_ids_query( $order_by, $order ) {
+		global $wpdb;
+
+		$query = "SELECT l.listing_id FROM {$wpdb->prefix}wpbdp_listings l";
+
+		switch ( $order_by ) {
+			case 'title':
+			case 'date':
+			case 'modified':
+			case 'author':
+				$query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id";
+				$query .= ' WHERE l.is_sticky=1';
+				$query .= " ORDER BY p.post_{$order_by} {$order}";
+
+				break;
+			case 'paid':
+			case 'paid-title':
+				$next_order = 'paid' === $order_by ? 'post_date DESC' : 'post_title ASC';
+
+				$query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id";
+				$query .= ' WHERE l.is_sticky=1';
+				$query .= " ORDER BY l.fee_price {$order}, p.{$next_order}";
+
+				break;
+			case 'plan-order-date':
+			case 'plan-order-title':
+				$plan_order = wpbdp_get_option( 'fee-order' );
+				if ( is_array( $plan_order ) && isset( $plan_order['method'] ) && 'custom' === $plan_order['method'] ) {
+					$next_order = 'plan-order-date' === $order_by ? 'post_date' : 'post_title';
+
+					$query .= " JOIN {$wpdb->posts} p ON p.ID = l.listing_id";
+					$query .= " LEFT JOIN {$wpdb->prefix}wpbdp_plans po ON po.id = l.fee_id";
+					$query .= ' WHERE l.is_sticky=1';
+					$query .= " ORDER BY po.weight DESC, p.{$next_order} {$order}";
+					break;
+				}
+
+				$query .= ' WHERE l.is_sticky=1';
+
+				break;
+			default:
+				$query .= ' WHERE l.is_sticky=1';
+
+				break;
+		}
+
+		return $query;
+	}
+
+	/**
+	 * Get the cache key for sticky listing ids.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $order_by The orderby value from the current query.
+	 * @param string $order    The normalized order direction.
+	 *
+	 * @return string
+	 */
+	private function get_sticky_listing_ids_cache_key( $order_by, $order ) {
+		return 'sticky_listing_ids_' . md5( $order_by . '|' . $order . '|' . maybe_serialize( wpbdp_get_option( 'fee-order' ) ) );
+	}
+
 	// {{ Sort bar.
 	public function sortbar_sort_options( $options ) {
 		$sortbar_fields = wpbdp_sortbar_get_field_options();
--- a/business-directory-plugin/includes/class-wpbdp.php
+++ b/business-directory-plugin/includes/class-wpbdp.php
@@ -57,7 +57,7 @@
 	}

 	private function setup_constants() {
-		define( 'WPBDP_VERSION', '6.4.24' );
+		define( 'WPBDP_VERSION', '6.4.25' );

 		define( 'WPBDP_PATH', wp_normalize_path( plugin_dir_path( WPBDP_PLUGIN_FILE ) ) );
 		define( 'WPBDP_INC', trailingslashit( WPBDP_PATH . 'includes' ) );
--- a/business-directory-plugin/includes/compatibility/class-wpml-compat.php
+++ b/business-directory-plugin/includes/compatibility/class-wpml-compat.php
@@ -45,9 +45,14 @@
 			add_action( 'wpbdp_query_flags', array( $this, 'maybe_change_query' ) );

 			add_action( 'wpbdp_before_ajax_dispatch', array( $this, 'before_ajax_dispatch' ) );
+
+			add_filter( 'wpbdp_form_field_value', array( $this, 'maybe_use_original_field_value' ), 10, 3 );
+			add_filter( 'wpbdp_form_field_html_value', array( $this, 'maybe_use_original_field_html_value' ), 10, 4 );
 		}

 		add_action( 'admin_footer', array( $this, 'maybe_register_some_strings' ) );
+		add_action( 'wpbdp_loaded', array( $this, 'register_custom_fields_with_wpml' ) );
+		add_action( 'wpml_translation_job_saved', array( $this, 'save_translated_field_values' ), 10, 3 );

 		// Regions.
 		add_filter( 'wpbdp_regions__get_hierarchy_option', array( &$this, 'use_cache_per_lang' ) );
@@ -405,7 +410,15 @@
 	}

 	public function translate_form_field_option_data( $value, $key, $field ) {
-		if ( ! is_object( $field ) || empty( $value ) || 'options' !== $key || ! function_exists( 'icl_t' ) || ! is_array( $value ) ) {
+		if ( ! is_object( $field ) ) {
+			return $value;
+		}
+
+		if ( 'supported_categories' === $key ) {
+			return $this->translate_supported_categories( $value );
+		}
+
+		if ( empty( $value ) || 'options' !== $key || ! function_exists( 'icl_t' ) || ! is_array( $value ) ) {
 			return $value;
 		}

@@ -550,4 +563,371 @@

 		echo '<input type="hidden" name="lang" value="' . esc_attr( $lang ) . '" />';
 	}
+
+	/**
+	 * Fall back to the original listing's field value when the translated post has none.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param mixed          $value   The field value (may be empty on translated posts).
+	 * @param int            $post_id The current listing post ID.
+	 * @param WPBDP_Form_Field $field The field object.
+	 *
+	 * @return mixed
+	 */
+	public function maybe_use_original_field_value( $value, $post_id, $field ) {
+		if ( 'meta' !== $field->get_association() || ! $field->is_empty_value( $value ) ) {
+			return $value;
+		}
+
+		$original_id = $this->get_original_listing_id( $post_id );
+
+		if ( ! $original_id || (int) $original_id === (int) $post_id ) {
+			return $value;
+		}
+
+		$original_value = $field->value( $original_id );
+
+		if ( $field->is_empty_value( $original_value ) ) {
+			return $value;
+		}
+
+		return $original_value;
+	}
+
+	/**
+	 * Fall back to the original listing's HTML value when a field type bypasses value filters.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string         $value           The field HTML value.
+	 * @param int            $post_id         The current listing post ID.
+	 * @param WPBDP_Form_Field $field         The field object.
+	 * @param string         $display_context The display context.
+	 *
+	 * @return string
+	 */
+	public function maybe_use_original_field_html_value( $value, $post_id, $field, $display_context = 'listing' ) {
+		if ( 'meta' !== $field->get_association() || ( null !== $value && '' !== $value ) ) {
+			return $value;
+		}
+
+		$original_id = $this->get_original_listing_id( $post_id );
+
+		if ( ! $original_id || (int) $original_id === (int) $post_id ) {
+			return $value;
+		}
+
+		return $field->html_value( $original_id, $display_context );
+	}
+
+	/**
+	 * Get the original (source) listing ID for a translated post.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param int $listing_id The translated listing post ID.
+	 *
+	 * @return int|null Original listing ID or null if already original.
+	 */
+	private function get_original_listing_id( $listing_id ) {
+		$element_type = 'post_' . WPBDP_POST_TYPE;
+
+		$trid = apply_filters( 'wpml_element_trid', null, $listing_id, $element_type );
+
+		if ( ! $trid ) {
+			return $this->get_original_listing_id_fallback( $listing_id );
+		}
+
+		$translations = apply_filters( 'wpml_get_element_translations', null, $trid, $element_type );
+
+		if ( ! is_array( $translations ) ) {
+			return $this->get_original_listing_id_fallback( $listing_id );
+		}
+
+		foreach ( $translations as $translation ) {
+			if ( ! empty( $translation->original ) ) {
+				return (int) $translation->element_id;
+			}
+		}
+
+		return null;
+	}
+
+	/**
+	 * Fallback to find original listing using default language lookup.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param int $listing_id The translated listing post ID.
+	 *
+	 * @return int|null
+	 */
+	private function get_original_listing_id_fallback( $listing_id ) {
+		$default_lang = apply_filters( 'wpml_default_language', null );
+
+		if ( ! $default_lang ) {
+			return null;
+		}
+
+		$original_id = apply_filters( 'wpml_object_id', $listing_id, WPBDP_POST_TYPE, true, $default_lang );
+
+		if ( $original_id && (int) $original_id !== (int) $listing_id ) {
+			return (int) $original_id;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Register BD custom field meta keys with WPML as translatable fields.
+	 *
+	 * @since 6.4.25
+	 */
+	public function register_custom_fields_with_wpml() {
+		if ( ! is_admin() || ! current_user_can( 'manage_options' ) ) {
+			return;
+		}
+
+		if ( ! defined( 'WPML_TRANSLATE_CUSTOM_FIELD' ) ) {
+			return;
+		}
+
+		$wpml_tm     = function_exists( 'wpml_load_core_tm' ) ? wpml_load_core_tm() : null;
+		$tm_settings = $wpml_tm ? $wpml_tm->get_settings() : $this->wpml->get_setting( 'translation-management', array() );
+
+		if ( ! is_array( $tm_settings ) ) {
+			$tm_settings = array();
+		}
+
+		if ( ! isset( $tm_settings['custom_fields_translation'] ) ) {
+			$tm_settings['custom_fields_translation'] = array();
+		}
+
+		$fields  = wpbdp_get_form_fields();
+		$updated = false;
+
+		foreach ( $fields as $field ) {
+			if ( 'meta' !== $field->get_association() ) {
+				continue;
+			}
+
+			$meta_key = '_wpbdp[fields][' . $field->get_id() . ']';
+
+			if ( ! isset( $tm_settings['custom_fields_translation'][ $meta_key ] ) || WPML_TRANSLATE_CUSTOM_FIELD !== (int) $tm_settings['custom_fields_translation'][ $meta_key ] ) {
+				$tm_settings['custom_fields_translation'][ $meta_key ] = WPML_TRANSLATE_CUSTOM_FIELD;
+				$updated = true;
+			}
+
+			if ( in_array( $field->get_field_type_id(), array( 'select', 'multiselect', 'checkbox', 'radio' ), true ) && defined( 'WPML_COPY_CUSTOM_FIELD' ) ) {
+				if ( ! isset( $tm_settings['custom_fields_translation'][ $meta_key . '_selected' ] ) || WPML_COPY_CUSTOM_FIELD !== (int) $tm_settings['custom_fields_translation'][ $meta_key . '_selected' ] ) {
+					$tm_settings['custom_fields_translation'][ $meta_key . '_selected' ] = WPML_COPY_CUSTOM_FIELD;
+					$updated = true;
+				}
+			}
+		}
+
+		if ( ! $updated ) {
+			return;
+		}
+
+		if ( $wpml_tm ) {
+			$wpml_tm->settings = $tm_settings;
+			$wpml_tm->save_settings();
+			return;
+		}
+
+		$this->wpml->set_setting( 'translation-management', $tm_settings, true );
+	}
+
+	/**
+	 * Translate field category restrictions to the active language.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param mixed $categories The supported categories field setting.
+	 *
+	 * @return mixed
+	 */
+	private function translate_supported_categories( $categories ) {
+		if ( 'all' === $categories || ! is_array( $categories ) ) {
+			return $categories;
+		}
+
+		$lang = $this->get_current_language();
+
+		if ( ! $lang ) {
+			return $categories;
+		}
+
+		$translated_categories = $categories;
+
+		foreach ( $categories as $category_id ) {
+			$translated_id = apply_filters( 'wpml_object_id', $category_id, WPBDP_CATEGORY_TAX, false, $lang );
+
+			if ( $translated_id ) {
+				$translated_categories[] = (int) $translated_id;
+			}
+		}
+
+		return array_unique( array_map( 'intval', $translated_categories ) );
+	}
+
+	/**
+	 * Save translated BD field values from WPML jobs.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param int    $post_id The translated listing post ID.
+	 * @param array  $fields  Job fields.
+	 * @param object $job     WPML translation job.
+	 */
+	public function save_translated_field_values( $post_id, $fields, $job ) {
+		unset( $fields );
+
+		if ( WPBDP_POST_TYPE !== get_post_type( $post_id ) || empty( $job->elements ) ) {
+			return;
+		}
+
+		foreach ( wpbdp_get_form_fields() as $field ) {
+			if ( 'meta' !== $field->get_association() ) {
+				continue;
+			}
+
+			$meta_key = '_wpbdp[fields][' . $field->get_id() . ']';
+			$values   = $this->get_translated_custom_field_values_from_job( $meta_key, $job );
+
+			if ( empty( $values ) ) {
+				continue;
+			}
+
+			$meta_value = 1 === count( $values ) ? reset( $values ) : $values;
+			update_post_meta( $post_id, $meta_key, $meta_value );
+		}
+	}
+
+	/**
+	 * Get translated custom field values from a WPML job without relying on WPML's bracket-sensitive regex.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $meta_key The custom field meta key.
+	 * @param object $job      WPML translation job.
+	 *
+	 * @return array
+	 */
+	private function get_translated_custom_field_values_from_job( $meta_key, $job ) {
+		$values = array();
+
+		foreach ( $job->elements as $element ) {
+			if ( empty( $element->field_type ) || empty( $element->field_data ) || substr( $element->field_type, -5 ) !== '-name' ) {
+				continue;
+			}
+
+			if ( 0 !== strpos( $element->field_data, $meta_key . '-' ) ) {
+				continue;
+			}
+
+			$field_id_string = substr( $element->field_type, 6, -5 );
+			$translated      = $this->get_job_element_translation( 'field-' . $field_id_string, $job );
+
+			if ( null === $translated ) {
+				continue;
+			}
+
+			$path = substr( $element->field_data, strlen( $meta_key ) + 1 );
+			$path = array_map( array( $this, 'decode_wpml_job_path_part' ), explode( '-', $path ) );
+
+			$this->set_array_path_value( $values, $path, $translated );
+		}
+
+		return $values;
+	}
+
+	/**
+	 * Get a translated WPML job element value.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $field_type The job field type.
+	 * @param object $job        WPML translation job.
+	 *
+	 * @return string|null
+	 */
+	private function get_job_element_translation( $field_type, $job ) {
+		foreach ( $job->elements as $element ) {
+			if ( $field_type !== $element->field_type ) {
+				continue;
+			}
+
+			$field_format = isset( $element->field_format ) ? $element->field_format : '';
+			return $this->decode_wpml_job_value( $element->field_data_translated, $field_format );
+		}
+
+		return null;
+	}
+
+	/**
+	 * Decode a WPML job value.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $value  The encoded value.
+	 * @param string $format The value format.
+	 *
+	 * @return string
+	 */
+	private function decode_wpml_job_value( $value, $format ) {
+		if ( 'base64' === $format ) {
+			$decoded = base64_decode( $value, true );
+
+			if ( false !== $decoded ) {
+				return $decoded;
+			}
+		}
+
+		$charset = get_bloginfo( 'charset' );
+
+		if ( ! $charset ) {
+			$charset = 'UTF-8';
+		}
+
+		return html_entity_decode( str_replace( '&#0A;', "n", $value ), ENT_QUOTES | ENT_HTML5, $charset );
+	}
+
+	/**
+	 * Decode a WPML field path part.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $path_part The encoded path part.
+	 *
+	 * @return string
+	 */
+	private function decode_wpml_job_path_part( $path_part ) {
+		return str_replace( ':::', '-', $path_part );
+	}
+
+	/**
+	 * Set a nested array value by path.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param array  $values The values array.
+	 * @param array  $path   The path to set.
+	 * @param string $value  The translated value.
+	 */
+	private function set_array_path_value( &$values, $path, $value ) {
+		$current = &$values;
+
+		foreach ( $path as $path_part ) {
+			if ( ! isset( $current[ $path_part ] ) ) {
+				$current[ $path_part ] = array();
+			}
+
+			$current = &$current[ $path_part ];
+		}
+
+		$current = $value;
+	}
 }
--- a/business-directory-plugin/includes/controllers/class-addons-controller.php
+++ b/business-directory-plugin/includes/controllers/class-addons-controller.php
@@ -104,7 +104,10 @@
 		// Create the plugin upgrader with our custom skin.
 		require_once WPBDP_INC . 'models/class-installer-skin.php';
 		$installer = new Plugin_Upgrader( new WPBDP_Installer_Skin() );
+		// phpcs:ignore WordPressVIPMinimum.Hooks.RestrictedHooks.http_request_args
+		add_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10, 2 );
 		$installer->install( $download_url );
+		remove_filter( 'http_request_args', 'wpbdp_add_user_agent_to_business_directory_request', 10 );

 		// Flush the cache and return the newly installed plugin basename.
 		wp_cache_flush();
--- a/business-directory-plugin/includes/fields/class-fieldtypes-radiobutton.php
+++ b/business-directory-plugin/includes/fields/class-fieldtypes-radiobutton.php
@@ -14,6 +14,15 @@
  */
 class WPBDP_FieldTypes_RadioButton extends WPBDP_Form_Field_Type {

+	/**
+	 * Meta radio inputs rejected during conversion.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @var array<int, bool>
+	 */
+	private $invalid_meta_inputs = array();
+
 	public function __construct() {
 		parent::__construct( _x( 'Radio button', 'form-fields api', 'business-directory-plugin' ) );
 	}
@@ -22,6 +31,51 @@
 		return 'radio';
 	}

+	/**
+	 * Convert submitted radio field input to a safe stored value.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param WPBDP_Form_Field $field Field object.
+	 * @param mixed            $input Submitted value.
+	 *
+	 * @return int|string
+	 */
+	public function convert_input( &$field, $input ) {
+		if ( 'meta' === $field->get_association() ) {
+			unset( $this->invalid_meta_inputs[ $field->get_id() ] );
+		}
+
+		if ( is_null( $input ) || '' === $input ) {
+			return '';
+		}
+
+		if ( ! is_scalar( $input ) ) {
+			if ( 'meta' === $field->get_association() ) {
+				$this->invalid_meta_inputs[ $field->get_id() ] = true;
+			}
+
+			return '';
+		}
+
+		$input = (string) $input;
+
+		if ( 'category' === $field->get_association() ) {
+			return absint( $input );
+		}
+
+		if ( 'meta' === $field->get_association() ) {
+			if ( in_array( $input, $this->get_stored_options( $field ), true ) ) {
+				return $input;
+			}
+
+			$this->invalid_meta_inputs[ $field->get_id() ] = true;
+			return '';
+		}
+
+		return sanitize_text_field( $input );
+	}
+
 	public function render_field_inner( &$field, $value, $context, &$extra = null, $field_settings = array() ) {
 		$options = $field->data( 'options' ) ? $field->data( 'options' ) : array();

@@ -66,20 +120,22 @@
 			$css_classes[] = 'wpbdp-inner-radio-' . $i;
 			$css_classes[] = 'wpbdp-inner-radio-' . WPBDP_Form_Field_Type::normalize_name( $label );

-			$checked = '';
+			$checked      = '';
+			$option_value = $field->get_association() === 'meta' ? $label : $option;
+			$field_id     = 'wpbdp-field-' . $field->get_id() . '-' . WPBDP_Form_Field_Type::normalize_name( $label );

-			if ( $selected === $option || $value === $option ) {
+			if ( ( false !== $selected && null !== $selected && (string) $selected === (string) $option ) || (string) $value === (string) $option_value ) {
 				$checked = 'checked="checked"';
 			}

 			$html .= sprintf(
-				'<div class="%1$s"><label for="wpbdp-field-%6$d-%5$s"><input id="wpbdp-field-%6$d-%5$s" type="radio" name="%2$s" value="%3$s" %4$s /> %5$s</label></div>',
-				implode( ' ', $css_classes ),
-				'listingfields[' . $field->get_id() . ']',
-				$field->get_association() === 'meta' ? esc_attr( $label ) : $option,
+				'<div class="%1$s"><label for="%6$s"><input id="%6$s" type="radio" name="%2$s" value="%3$s" %4$s /> %5$s</label></div>',
+				esc_attr( implode( ' ', $css_classes ) ),
+				esc_attr( 'listingfields[' . $field->get_id() . ']' ),
+				esc_attr( $option_value ),
 				$checked,
-				esc_attr( $label ),
-				$field->get_id()
+				esc_html( $label ),
+				esc_attr( $field_id )
 			);

 			++$i;
@@ -101,7 +157,18 @@
 	 */
 	public function store_field_value( &$field, $post_id, $value ) {
 		if ( $field->get_association() === 'meta' ) {
-			$this->store_field_selected_value( $field, $post_id, $value );
+			$is_invalid_input = isset( $this->invalid_meta_inputs[ $field->get_id() ] );
+			unset( $this->invalid_meta_inputs[ $field->get_id() ] );
+
+			if ( $is_invalid_input ) {
+				return;
+			}
+
+			if ( '' !== $value && in_array( (string) $value, $this->get_stored_options( $field ), true ) ) {
+				$this->store_field_selected_value( $field, $post_id, $value );
+			} else {
+				delete_post_meta( $post_id, '_wpbdp[fields][' . $field->get_id() . ']_selected' );
+			}
 		}

 		parent::store_field_value( $field, $post_id, $value );
@@ -189,6 +256,27 @@
 		return $value && is_array( $value ) ? $value[0] : $value;
 	}

+	/**
+	 * Return escaped HTML-safe output for radio field values.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param WPBDP_Form_Field $field   Field object.
+	 * @param int|string       $post_id Listing ID.
+	 *
+	 * @return string
+	 */
+	public function get_field_html_value( &$field, $post_id ) {
+		if ( 'meta' === $field->get_association() ) {
+			$value = $field->value( $post_id );
+			$value = is_array( $value ) ? implode( ', ', $value ) : $value;
+
+			return esc_html( (string) $value );
+		}
+
+		return parent::get_field_html_value( $field, $post_id );
+	}
+
 	public function get_field_plain_value( &$field, $post_id ) {
 		$value = $field->value( $post_id );

@@ -202,4 +290,19 @@

 		return strval( $value );
 	}
+
+	/**
+	 * Return configured radio options as stored string values.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param WPBDP_Form_Field $field Field object.
+	 *
+	 * @return string[]
+	 */
+	private function get_stored_options( $field ) {
+		$options = $field->data( 'options' ) ? $field->data( 'options' ) : array();
+
+		return array_map( 'strval', $options );
+	}
 }
--- a/business-directory-plugin/includes/gateways/stripe/helpers/classStrpConnectHelper.php
+++ b/business-directory-plugin/includes/gateways/stripe/helpers/classStrpConnectHelper.php
@@ -95,8 +95,9 @@
 		$timeout = 45; // (seconds) default timeout is 5. we want a bit more time to work with.
 		self::try_to_extend_server_timeout( $timeout );

-		$args     = compact( 'body', 'headers', 'timeout' );
-		$response = wp_remote_post( $url, $args );
+		$args               = compact( 'body', 'headers', 'timeout' );
+		$args['user-agent'] = wpbdp_http_user_agent();
+		$response           = wp_remote_post( $url, $args );

 		if ( ! self::validate_response( $response ) ) {
 			return 'Response from server is invalid';
--- a/business-directory-plugin/includes/licensing.php
+++ b/business-directory-plugin/includes/licensing.php
@@ -497,6 +497,8 @@

 		curl_setopt( $ch, CURLOPT_URL, self::STORE_URL );
 		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
+		// phpcs:ignore WordPress.WP.AlternativeFunctions.curl_curl_setopt
+		curl_setopt( $ch, CURLOPT_USERAGENT, wpbdp_http_user_agent() );

 		$r = curl_exec( $ch );

@@ -593,7 +595,7 @@
 			$url,
 			array(
 				'timeout'    => 15,
-				'user-agent' => $this->user_agent_header(),
+				'user-agent' => wpbdp_http_user_agent(),
 				'sslverify'  => false,
 			)
 		);
@@ -1008,7 +1010,7 @@
 			self::STORE_URL,
 			array(
 				'timeout'    => 15,
-				'user-agent' => $this->user_agent_header(),
+				'user-agent' => wpbdp_http_user_agent(),
 				'sslverify'  => false,
 				'body'       => $args,
 			)
@@ -1261,11 +1263,7 @@
 	}

 	function user_agent_header() {
-		$user_agent  = 'Business Directory/' . WPBDP_VERSION;
-		$user_agent .= ' WordPress/' . get_bloginfo( 'version' );
-		$user_agent .= '; ' . get_bloginfo( 'url' );
-
-		return $user_agent;
+		return wpbdp_http_user_agent();
 	}

 	/**
--- a/business-directory-plugin/includes/models/class-sitetracking.php
+++ b/business-directory-plugin/includes/models/class-sitetracking.php
@@ -132,9 +132,10 @@
 			wp_remote_post(
 				self::TRACKING_URL,
 				array(
-					'method'   => 'POST',
-					'blocking' => false,
-					'body'     => $data,
+					'method'     => 'POST',
+					'blocking'   => false,
+					'user-agent' => wpbdp_http_user_agent(),
+					'body'       => $data,
 				)
 			);
 		}
@@ -175,9 +176,10 @@
 			wp_remote_post(
 				self::TRACKING_URL,
 				array(
-					'method'   => 'POST',
-					'blocking' => true,
-					'body'     => array(
+					'method'     => 'POST',
+					'blocking'   => true,
+					'user-agent' => wpbdp_http_user_agent(),
+					'body'       => array(
 						'uninstall' => '1',
 						'hash'      => $hash,
 						'reason'    => $reason,
--- a/business-directory-plugin/includes/themes.php
+++ b/business-directory-plugin/includes/themes.php
@@ -666,10 +666,11 @@
 			'variables' => 'Template Variables',
 		);
 		$template_meta   = get_file_data( $template_path, $default_headers, 'business_directory_template' );
+		$legacy_meta     = $this->get_legacy_template_meta( $template_path );

 		foreach ( array_keys( $default_headers ) as $variable ) {
 			if ( ! $template_meta[ $variable ] ) {
-				$template_meta[ $variable ] = array();
+				$template_meta[ $variable ] = $legacy_meta[ $variable ];
 				continue;
 			}

@@ -679,6 +680,63 @@
 		return $template_meta;
 	}

+	/**
+	 * Gets template metadata from the old `$__template__` declaration.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $template_path Path to the template file.
+	 *
+	 * @return array
+	 */
+	private function get_legacy_template_meta( $template_path ) {
+		$template_meta = array(
+			'blocks'    => array(),
+			'variables' => array(),
+		);
+
+		if ( ! is_readable( $template_path ) ) {
+			return $template_meta;
+		}
+
+		try {
+			$template_file     = new SplFileObject( $template_path, 'r' );
+			$template_contents = $template_file->fread( 8192 );
+		} catch ( RuntimeException $e ) {
+			return $template_meta;
+		}
+
+		if ( ! preg_match( '/$__template__s*=s*arrays*((.*?))s*;/s', $template_contents, $template_match ) ) {
+			return $template_meta;
+		}
+
+		foreach ( array_keys( $template_meta ) as $variable ) {
+			$pattern = '/['"]' . preg_quote( $variable, '/' ) . '['"]s*=>s*arrays*(([^)]*))/s';
+			if ( preg_match( $pattern, $template_match[1], $variable_match ) ) {
+				$template_meta[ $variable ] = $this->parse_legacy_template_meta_values( $variable_match[1] );
+			}
+		}
+
+		return $template_meta;
+	}
+
+	/**
+	 * Extracts quoted values from old template metadata arrays.
+	 *
+	 * @since 6.4.25
+	 *
+	 * @param string $values List of values from a legacy template metadata array.
+	 *
+	 * @return array
+	 */
+	private function parse_legacy_template_meta_values( $values ) {
+		if ( ! preg_match_all( '/['"]([A-Za-z0-9_-]+)['"]/', $values, $matches ) ) {
+			return array();
+		}
+
+		return array_values( array_unique( $matches[1] ) );
+	}
+
 	function render_part( $template_id, $additional_vars = array() ) {
 		$output = '';

--- a/business-directory-plugin/includes/utils.php
+++ b/business-directory-plugin/includes/utils.php
@@ -713,6 +713,75 @@
 }

 /**
+ * Build a Business Directory user agent for outbound HTTP requests.
+ *
+ * @since 6.4.25
+ *
+ * @param string $product Product name.
+ * @param string|null $version Product version.
+ *
+ * @return string
+ */
+function wpbdp_http_user_agent( $product = 'Business Directory', $version = null ) {
+	$product = (string) $product;
+
+	if ( '' === $product ) {
+		$product = 'Business Directory';
+	}
+
+	if ( null === $version && defined( 'WPBDP_VERSION' ) ) {
+		$version = WPBDP_VERSION;
+	}
+
+	$version    = (string) $version;
+	$user_agent = $product;
+	if ( '' !== $version ) {
+		$user_agent .= '/' . $version;
+	}
+
+	return $user_agent . '; ' . get_bloginfo( 'url' );
+}
+
+/**
+ * Check if a URL points to the Business Directory domain.
+ *
+ * @since 6.4.25
+ *
+ * @param string $url The URL to check.
+ *
+ * @return bool
+ */
+function wpbdp_is_business_directory_request_url( $url ) {
+	$host = wp_parse_url( $url, PHP_URL_HOST );
+	if ( ! is_string( $host ) || '' === $host ) {
+		return false;
+	}
+
+	$host   = strtolower( $host );
+	$domain = 'businessdirectoryplugin.com';
+
+	return $domain === $host || substr( $host, -1 * ( strlen( $domain ) + 1 ) ) === '.' . $domain;
+}
+
+/**
+ * Add the Business Directory user agent to package downloads from our servers.
+ *
+ * @since 6.4.25
+ *
+ * @param array  $args HTTP request args.
+ * @param string $url  Request URL.
+ *
+ * @return array
+ */
+function wpbdp_add_user_agent_to_business_directory_request( $args, $url ) {
+	if ( wpbdp_is_business_directory_request_url( $url ) ) {
+		$args['user-agent'] = wpbdp_http_user_agent();
+	}
+
+	return $args;
+}
+
+/**
  * Prepare an external link with utm parameters.
  *
  * @since 5.7.5

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.