--- a/easy-property-listings/easy-property-listings.php
+++ b/easy-property-listings/easy-property-listings.php
@@ -5,7 +5,7 @@
* Description: Fast. Flexible. Forward-thinking solution for real estate agents using WordPress. Easy Property Listing is one of the most dynamic and feature rich Real Estate plugin for WordPress available on the market today. Built for scale, contact generation and works with any theme!
* Author: Merv Barrett
* Author URI: https://www.realestateconnected.com.au/
- * Version: 3.5.20
+ * Version: 3.5.21
* Text Domain: easy-property-listings
* Domain Path: languages
*
@@ -25,7 +25,7 @@
* @package EPL
* @category Core
* @author Merv Barrett
- * @version 3.5.20
+ * @version 3.5.21
*/
// Exit if accessed directly.
@@ -118,7 +118,7 @@
public function setup_constants() {
// Plugin version.
if ( ! defined( 'EPL_PROPERTY_VER' ) ) {
- define( 'EPL_PROPERTY_VER', '3.5.20' );
+ define( 'EPL_PROPERTY_VER', '3.5.21' );
}
// Plugin DB version.
if ( ! defined( 'EPL_PROPERTY_DB_VER' ) ) {
--- a/easy-property-listings/lib/assets/assets-svg.php
+++ b/easy-property-listings/lib/assets/assets-svg.php
@@ -411,6 +411,7 @@
* Initiate EPL listings & social Svgs.
*
* @since 3.4.32
+ * @since 3.5.21 Divi support for SVG icons when using Divi custom header.
*/
function epl_init_svgs() {
@@ -419,19 +420,50 @@
$epl_social_svgs_loaded = false;
/**
- * Load SVG using wp_body_open introduced in wp 5.2
+ * Detect Divi.
*
- * @since 3.4.31
+ * We only switch away from wp_body_open if we're confident Divi is actually active.
*/
- add_action( 'wp_body_open', 'epl_load_svg_listing_icons_head', 10 );
- add_action( 'wp_footer', 'epl_load_svg_listing_icons_head', 900 );
+ $theme = null;
+ $template = '';
+ $name = '';
+
+ if ( function_exists( 'wp_get_theme' ) ) {
+ $theme = wp_get_theme();
+ $template = strtolower( (string) $theme->get_template() );
+ $name = strtolower( (string) $theme->get( 'Name' ) );
+ }
+
+ $is_divi = (
+ 'divi' === $template ||
+ false !== strpos( $name, 'divi' ) ||
+ defined( 'ET_BUILDER_VERSION' ) ||
+ function_exists( 'et_setup_theme' ) ||
+ function_exists( 'et_divi_load_scripts_styles' )
+ );
+
+ $is_divi = apply_filters( 'epl_is_divi_theme', $is_divi );
/**
- * Load Social SVG using wp_body_open introduced in wp 5.2
- *
- * @since 3.4.31
+ * Hook strategy:
+ * - Default: wp_body_open + wp_footer fallback
+ * - Divi: et_body_top + wp_footer fallback (avoid wp_body_open swallowing output but still setting globals)
*/
- add_action( 'wp_body_open', 'epl_load_svg_social_icons_head', 10 );
+ if ( $is_divi ) {
+
+ // Divi body-top hook.
+ add_action( 'et_body_top', 'epl_load_svg_listing_icons_head', 10 );
+ add_action( 'et_body_top', 'epl_load_svg_social_icons_head', 10 );
+
+ } else {
+
+ // Standard WP hook.
+ add_action( 'wp_body_open', 'epl_load_svg_listing_icons_head', 10 );
+ add_action( 'wp_body_open', 'epl_load_svg_social_icons_head', 10 );
+ }
+
+ // Always keep footer fallback.
+ add_action( 'wp_footer', 'epl_load_svg_listing_icons_head', 900 );
add_action( 'wp_footer', 'epl_load_svg_social_icons_head', 900 );
}
add_action( 'wp', 'epl_init_svgs' );
--- a/easy-property-listings/lib/includes/admin/admin-functions.php
+++ b/easy-property-listings/lib/includes/admin/admin-functions.php
@@ -354,21 +354,43 @@
}
/**
- * Un-serialize Variable
+ * Safely unserialize base64 encoded data.
*
- * @param string $data String of data to serialize.
+ * This helper decodes a base64 encoded string and attempts to unserialize it
+ * while applying several validation steps to reduce security risks.
+ *
+ * Security improvements:
+ * - Uses strict base64 decoding to prevent malformed input.
+ * - Validates that the decoded value is actually a serialized string before
+ * attempting to unserialize it.
+ * - Prevents object injection by disabling object instantiation via the
+ * `allowed_classes => false` option.
+ *
+ * If the input cannot be decoded or is not a valid serialized value, the
+ * function safely returns false instead of attempting to unserialize it.
*
- * @return mixed un-serialized string.
* @since 3.3.0
+ * @since 3.5.21 Hardened unserialize handling by enforcing strict base64 decoding, validating serialized input, and disabling object instantiation.
+ *
+ * @param string $data Base64 encoded serialized data.
+ * @return mixed|false Returns the unserialized value on success, or false if the
+ * input is invalid or cannot be safely unserialized.
*/
function epl_unserialize( $data ) {
- return unserialize( base64_decode( $data ) ); //phpcs:ignore
+ $decoded_data = base64_decode( trim( (string) $data ), true );
+
+ if ( false === $decoded_data || ! is_serialized( $decoded_data ) ) {
+ return false;
+ }
+
+ return unserialize( $decoded_data, array( 'allowed_classes' => false ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
}
/**
- * Import Tools Settings Screen
+ * Import/Export Tools Settings Screen
*
* @since 3.3
+ * @since 3.5.21 Added nonce protection to the export request to prevent CSRF.
*/
function epl_settings_import_export() {
@@ -410,7 +432,18 @@
$tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'tools';
- echo "<a class='button button-primary' href='" . esc_url( '?page=epl-tools&tab=$tab&action=export&epl_tools_submit=true' ) . "'>" . esc_html__( 'Download File', 'easy-property-listings' ) . '</a>';
+ $export_url = add_query_arg(
+ array(
+ 'page' => 'epl-tools',
+ 'tab' => $tab,
+ 'action' => 'export',
+ 'epl_tools_submit' => 'true',
+ ),
+ admin_url( 'admin.php' )
+ );
+ $export_url = wp_nonce_url( $export_url, 'epl_tools_export', 'epl_tools_export_nonce' );
+
+ echo "<a class='button button-primary' href='" . esc_url( $export_url ) . "'>" . esc_html__( 'Download File', 'easy-property-listings' ) . '</a>';
?>
<span style="color:#f00"><?php esc_html_e( 'The following settings are exported. Easy Property Listings settings screen and any Extension settings', 'easy-property-listings' ); ?></span>
<?php
@@ -474,9 +507,15 @@
* @since 3.3.0
* @since 3.5 Fixed import function.
* @since 3.5.10 Fix: Tools Import function adjusted with more checked before performing the settings import.
+ * @since 3.5.21 Hardened tools request handling with capability checks, stricter sanitization, action allowlisting, and export nonce verification.
*/
function epl_handle_tools_form() {
- if ( ! isset( $_GET['page'] ) || 'epl-tools' !== $_GET['page'] || ! isset( $_REQUEST['epl_tools_submit'] ) ) {
+ $page = isset( $_REQUEST['page'] ) ? sanitize_key( wp_unslash( $_REQUEST['page'] ) ) : '';
+ if ( 'epl-tools' !== $page || ! isset( $_REQUEST['epl_tools_submit'] ) ) {
+ return;
+ }
+
+ if ( ! current_user_can( 'manage_options' ) ) {
return;
}
@@ -484,15 +523,20 @@
return;
}
- $action = sanitize_text_field( wp_unslash( $_REQUEST['action'] ) );
+ $action = sanitize_key( wp_unslash( $_REQUEST['action'] ) );
+ if ( ! in_array( $action, array( 'export', 'import', 'reset' ), true ) ) {
+ return;
+ }
+
+ if ( 'export' === $action ) {
+ epl_verify_export_nonce();
+ }
if ( 'import' === $action ) {
epl_verify_nonce();
epl_validate_import_file();
}
- $post_data = filter_input_array( INPUT_POST, FILTER_SANITIZE_STRING );
-
switch ( $action ) {
case 'export':
epl_export_settings();
@@ -510,6 +554,20 @@
add_action( 'admin_init', 'epl_handle_tools_form' );
/**
+ * Verify nonce for export tools action.
+ *
+ * @since 3.5.20
+ */
+function epl_verify_export_nonce() {
+ if (
+ ! isset( $_GET['epl_tools_export_nonce'] ) ||
+ ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['epl_tools_export_nonce'] ) ), 'epl_tools_export' )
+ ) {
+ wp_die( esc_html__( 'Sorry, your nonce did not verify.', 'easy-property-listings' ) );
+ }
+}
+
+/**
* Verify nonce for the tools form.
*
* @since 3.5.10
@@ -527,6 +585,7 @@
* Validate the import file.
*
* @since 3.5.10
+ * @since 3.5.21 Added stricter import upload validation (uploaded file checks, size limits, and file type/extension verification).
*/
function epl_validate_import_file() {
@@ -534,11 +593,24 @@
wp_die( esc_html__( 'Missing import file. Please provide an import file.', 'easy-property-listings' ) );
}
+ $file_name = sanitize_file_name( wp_unslash( $_FILES['epl_import']['name'] ) );
+ $tmp_name = isset( $_FILES['epl_import']['tmp_name'] ) ? wp_unslash( $_FILES['epl_import']['tmp_name'] ) : '';
+ $file_size = isset( $_FILES['epl_import']['size'] ) ? (int) $_FILES['epl_import']['size'] : 0;
+
if ( isset( $_FILES['epl_import']['error'] ) && $_FILES['epl_import']['error'] > 0 ) {
wp_die( esc_html__( 'Error uploading the import file.', 'easy-property-listings' ) );
}
- if ( empty( $_FILES['epl_import']['type'] ) || ! in_array( strtolower( $_FILES['epl_import']['type'] ), array( 'text/plain' ), true ) ) {
+ if ( empty( $tmp_name ) || ! is_uploaded_file( $tmp_name ) || ! is_readable( $tmp_name ) ) {
+ wp_die( esc_html__( 'Invalid import upload.', 'easy-property-listings' ) );
+ }
+
+ if ( $file_size <= 0 || $file_size > wp_max_upload_size() ) {
+ wp_die( esc_html__( 'The selected import file size is not valid.', 'easy-property-listings' ) );
+ }
+
+ $file_check = wp_check_filetype_and_ext( $tmp_name, $file_name, array( 'txt' => 'text/plain' ) );
+ if ( empty( $file_check['ext'] ) || 'txt' !== strtolower( $file_check['ext'] ) ) {
wp_die( esc_html__( 'The file you uploaded does not appear to be a valid import file.', 'easy-property-listings' ) );
}
}
@@ -569,21 +641,31 @@
*
* @since 3.5.10
* @since 3.5.18 Check for data before continue.
+ * @since 3.5.21 Hardened import processing by reading from the uploaded temp file, validating file contents, and verifying unserialized data before updating options.
*/
function epl_import_settings() {
- if ( ! isset( $_FILES['epl_import'] ) ) {
+ if ( ! isset( $_FILES['epl_import']['tmp_name'] ) ) {
return;
}
- $upload_overrides = array( 'test_form' => false );
- $movefile = wp_handle_upload( $_FILES['epl_import'], $upload_overrides ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- handled by wp_handle_upload.
- if ( $movefile && ! isset( $movefile['error'] ) ) {
- $imported_data = epl_remote_url_get( $movefile['url'] );
- $imported_data = epl_unserialize( $imported_data );
- $options_backup = get_option( 'epl_settings' );
- update_option( 'epl_settings_backup', $options_backup );
- $status = update_option( 'epl_settings', $imported_data );
+ $tmp_name = wp_unslash( $_FILES['epl_import']['tmp_name'] );
+ if ( ! is_readable( $tmp_name ) ) {
+ wp_die( esc_html__( 'Unable to read import file.', 'easy-property-listings' ) );
+ }
+
+ $imported_raw_data = file_get_contents( $tmp_name ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
+ if ( false === $imported_raw_data || '' === $imported_raw_data ) {
+ wp_die( esc_html__( 'Unable to read import file.', 'easy-property-listings' ) );
+ }
+
+ $imported_data = epl_unserialize( $imported_raw_data );
+ if ( ! is_array( $imported_data ) ) {
+ wp_die( esc_html__( 'The import file data is invalid.', 'easy-property-listings' ) );
}
+
+ $options_backup = get_option( 'epl_settings' );
+ update_option( 'epl_settings_backup', $options_backup );
+ update_option( 'epl_settings', $imported_data );
}
/**
--- a/easy-property-listings/lib/includes/class-epl-license.php
+++ b/easy-property-listings/lib/includes/class-epl-license.php
@@ -583,6 +583,7 @@
*
* @access public
* @return void
+ * @since 3.5.21 Fix: License notice message sanitization adjusted to run after sprintf(). Thanks DAnn2012.
*/
public function notices() {
@@ -614,7 +615,7 @@
default:
// translators: error.
- $message = sprintf( wp_kses_post( __( 'There was a problem activating your license key, please try again or contact support. Error code: %s', 'easy-property-listings' ) ), $license_error->error );
+ $message = wp_kses_post( sprintf( __( 'There was a problem activating your license key, please try again or contact support. Error code: %s', 'easy-property-listings' ), $license_error->error ) );
break;
}
--- a/easy-property-listings/lib/includes/class-epl-property-meta.php
+++ b/easy-property-listings/lib/includes/class-epl-property-meta.php
@@ -250,6 +250,7 @@
* @since 3.5.3 Fix: Deprecation warning - Make sure inspection time is not null before passing through trim.
* @since 3.5.3 Update to use local timestamp.
* @since 3.5.13 Tweak: Target blank added to ical link.
+ * @since 3.5.21 Added a signed token to the iCal inspection link and switched URL generation to add_query_arg().
*/
public function get_property_inspection_times( $ical = true, $meta_key = 'property_inspection_times' ) {
if ( 'leased' === $this->get_property_meta( 'property_status' ) || 'sold' === $this->get_property_meta( 'property_status' ) ) {
@@ -303,7 +304,16 @@
} else {
- $href = get_bloginfo( 'url' ) . '?epl_cal_dl=1&cal=ical&dt=' . base64_encode( htmlspecialchars( $element, ENT_QUOTES, 'UTF-8' ) ) . '&propid=' . $this->post->ID;
+ $href = add_query_arg(
+ array(
+ 'epl_cal_dl' => 1,
+ 'cal' => 'ical',
+ 'dt' => base64_encode( htmlspecialchars( $element, ENT_QUOTES, 'UTF-8' ) ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
+ 'propid' => $this->post->ID,
+ 'k' => epl_get_ical_download_token( $this->post->ID, $element ),
+ ),
+ home_url( '/' )
+ );
$href = apply_filters( 'epl_inspection_link', $href );
--- a/easy-property-listings/lib/includes/functions.php
+++ b/easy-property-listings/lib/includes/functions.php
@@ -142,6 +142,24 @@
}
/**
+ * Generate signed token for inspection iCal links.
+ *
+ * @param int $post_id Post ID.
+ * @param string $inspection_time Inspection date/time string.
+ *
+ * @return string
+ * @since 3.5.21
+ */
+function epl_get_ical_download_token( $post_id, $inspection_time ) {
+ $post_id = absint( $post_id );
+ $inspection_time = trim( (string) $inspection_time );
+ $payload = $post_id . '|' . $inspection_time;
+ $secret = apply_filters( 'epl_ical_token_secret', wp_salt( 'nonce' ) );
+
+ return hash_hmac( 'sha256', $payload, $secret );
+}
+
+/**
* Register post type to EPL and WordPress
*
* @param string $post_type Post type name.
--- a/easy-property-listings/lib/includes/pagination.php
+++ b/easy-property-listings/lib/includes/pagination.php
@@ -356,16 +356,17 @@
* @since 3.3.3
* @since 3.5.1 Fixed shortcode pagination when permalinks are plain.
* @since 3.5.3 Fixed sorting not working for pagination on shortcode.
+ * @since 3.5.21 Tweak for epl-search-builder ajax pagination.
*/
function epl_get_next_page_link( $query ) {
$link = next_posts( $query->max_num_pages, false );
- if ( $query->get( 'is_epl_shortcode' ) &&
- in_array( $query->get( 'epl_shortcode_name' ), epl_get_shortcode_list(), true ) ) {
+ if ( ($query->get( 'is_epl_shortcode' ) &&
+ in_array( $query->get( 'epl_shortcode_name' ), epl_get_shortcode_list(), true ) ) || $query->is_epl_search ) {
$permalink_structure = get_option( 'permalink_structure' );
- if ( empty( $permalink_structure ) ) {
+ if ( empty( $permalink_structure ) || $query->is_epl_search ) {
$page = $query->get( 'paged' );
@@ -416,16 +417,17 @@
* @since 3.3.3
* @since 3.5.1 Fixed shortcode pagination when permalinks are plain.
* @since 3.5.3 Fixed sorting not working for pagination on shortcode.
+ * @since 3.5.21 Tweak for epl-search-builder ajax pagination.
*/
function epl_get_prev_page_link( $query ) {
$link = previous_posts( false );
- if ( $query->get( 'is_epl_shortcode' ) &&
- in_array( $query->get( 'epl_shortcode_name' ), epl_get_shortcode_list(), true ) ) {
+ if ( ($query->get( 'is_epl_shortcode' ) &&
+ in_array( $query->get( 'epl_shortcode_name' ), epl_get_shortcode_list(), true ) ) || $query->is_epl_search ) {
$permalink_structure = get_option( 'permalink_structure' );
- if ( empty( $permalink_structure ) ) {
+ if ( empty( $permalink_structure ) || $query->is_epl_search ) {
$page = $query->get( 'paged' );
@@ -446,14 +448,16 @@
/**
* Prev page Link
*
- * @since 3.3.3
* @param WP_Query $query WP Query object.
* @param string $label Pagination 'previous' label.
* @return string
+ *
+ * @since 3.3.3
+ * @since 3.5.21 Tweak for epl-search-builder ajax pagination.
*/
function epl_prev_post_link( $query, $label = null ) {
- global $paged;
+ $paged = $query->get( 'paged' );
if ( $paged > 1 ) {
--- a/easy-property-listings/lib/includes/template-functions.php
+++ b/easy-property-listings/lib/includes/template-functions.php
@@ -2332,6 +2332,7 @@
* @since 2.0.0
* @since 3.4.9 Corrected issue where output was trimmed, added better unique ID and URL to output.
* @since 3.5.7 Updated to allow passing of extra details to ical.
+ * @since 3.5.21 Sanitized generated iCal filename and added fallback filename handling.
*/
function epl_create_ical_file( $start = '', $end = '', $name = '', $description = '', $location = '', $post_id = null ) {
@@ -2343,12 +2344,16 @@
$uid = $post_id . time();
$url = get_permalink( $post_id );
$prodid = '-//' . get_bloginfo( 'name' ) . '/EPL//NONSGML v1.0//EN';
+ $file_name = sanitize_file_name( $name );
+ if ( '' === $file_name ) {
+ $file_name = 'event';
+ }
$args = get_defined_vars();
$args = apply_filters( 'epl_ical_args', $args );
$data = "BEGIN:VCALENDARnVERSION:2.0nPRODID:" . $args['prodid'] . "nMETHOD:PUBLISHnBEGIN:VEVENTnDTSTART:" . gmdate( 'YmdTHis', strtotime( $args['start'] ) ) . "nDTEND:" . gmdate( 'YmdTHis', strtotime( $args['end'] ) ) . "nLOCATION:" . $args['location'] . "nURL:" . $args['url'] . "nTRANSP:OPAQUEnSEQUENCE:0nUID:" . $args['uid'] . "nDTSTAMP:" . gmdate( 'YmdTHisZ' ) . "nSUMMARY:" . $args['name'] . "nDESCRIPTION:" . $args['description'] . "nPRIORITY:1nCLASS:PUBLICnBEGIN:VALARMnTRIGGER:-PT10080MnACTION:DISPLAYnDESCRIPTION:RemindernEND:VALARMnEND:VEVENTnEND:VCALENDARn";
header( 'Content-type:text/calendar' );
- header( 'Content-Disposition: attachment; filename="' . $name . '.ics"' );
+ header( 'Content-Disposition: attachment; filename="' . $file_name . '.ics"' );
Header( 'Content-Length: ' . strlen( $data ) );
Header( 'Connection: close' );
echo $data; //phpcs:ignore
@@ -2362,6 +2367,7 @@
* @since 3.5.7 Different subject for auction.
* @since 3.5.16 Triple equals for auction value.
* @since 3.5.20 ical access issue.
+ * @since 3.5.21 Added signed token validation for iCal download requests and introduced filterable iCal event description. iCal description now uses the excerpt instead of full content.
*/
function epl_process_event_cal_request() {
global $epl_settings;
@@ -2384,6 +2390,14 @@
return;
}
+ $token = isset( $_GET['k'] ) ? sanitize_text_field( wp_unslash( $_GET['k'] ) ) : '';
+ $valid = ! empty( $token ) && hash_equals( epl_get_ical_download_token( $post_id, $item ), $token );
+
+ $allow_legacy_access = apply_filters( 'epl_allow_legacy_ical_access', false, $post_id, $item );
+ if ( ! $valid && ! $allow_legacy_access && ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
$post = get_post( $post_id );
if ( is_null( $post ) || 'publish' !== $post->post_status || ! in_array( $post->post_type, epl_get_core_post_types(), true ) || post_password_required( $post_id ) ) {
return;
@@ -2443,7 +2457,10 @@
$address .= get_post_meta( $post_id, 'property_address_state', true ) . ' ';
$address .= get_post_meta( $post_id, 'property_address_postal_code', true );
- epl_create_ical_file( $starttime, $endtime, $subject, wp_strip_all_tags( $post->post_content ), $address, $post_id );
+ $description = wp_strip_all_tags( get_the_excerpt( $post ) );
+ $description = apply_filters( 'epl_ical_description', $description, $post_id, $post, $item );
+
+ epl_create_ical_file( $starttime, $endtime, $subject, $description, $address, $post_id );
}
add_action( 'init', 'epl_process_event_cal_request' );