Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- 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( '�A;', "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