PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.0 2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 All 262 releases
← All changes | includes/admin/class-ph-admin.php +254 -129 2.2.22.4.0 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 if ( ! defined( 'ABSPATH' ) ) {
3 6 exit; // Exit if accessed directly
4 7 }
5 8
@@ -11,8 +14,9 @@
11 14 * @category Admin
12 15 * @package PropertyHive/Admin
13 16 * @version 1.0.0
14 17 */
18 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin; preserving the existing PH_* class name is required for plugin and extension compatibility.
15 19 class PH_Admin {
16 20
17 21 /**
18 22 * Constructor
@@ -24,9 +28,8 @@
24 28 add_action( 'current_screen', array( $this, 'disable_propertyhive_meta_box_dragging' ) );
25 29 add_action( 'current_screen', array( $this, 'remove_propertyhive_meta_boxes_from_screen_options' ) );
26 30 add_action( 'admin_notices', array( $this, 'review_admin_notices') );
27 31 add_action( 'admin_notices', array( $this, 'archive_admin_notices' ) );
28 - //add_action( 'admin_notices', array( $this, 'retired_template_assistant_admin_notices' ) );
29 32 add_action( 'admin_menu', array( $this, 'admin_dashboard_pages' ) );
30 33 add_action( 'admin_head', array( $this, 'admin_head' ) );
31 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
32 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
@@ -41,68 +44,87 @@
41 44 }
42 45
43 46 public function archive_admin_notices()
44 47 {
48 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
45 49 if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts']))
46 50 {
47 - $post_type = isset($_GET['post_type']) ? $_GET['post_type'] : '';
51 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
52 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
48 53 if ( $post_type )
49 54 {
50 55 $post_type_object = get_post_type_object($post_type);
56 + if ( ! $post_type_object ) {
57 + return;
58 + }
51 59
52 - $count = intval($_GET['bulk_archived_posts']);
60 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
61 + $count = is_string($_GET['bulk_archived_posts']) ? absint($_GET['bulk_archived_posts']) : 0;
53 62
54 - printf(
55 - '<div id="message" class="notice is-dismissible updated"><p>' . _n('%s ' . $post_type_object->labels->singular_name . ' moved to Archive.', '%s ' . $post_type_object->labels->name . ' moved to Archive.', $count, 'propertyhive') . '</p></div>',
56 - $count
57 - );
63 + if ( $post_type_object )
64 + {
65 + $message = sprintf(
66 + /* translators: 1: number of items, 2: post type label */
67 + _n(
68 + '%1$s %2$s moved to archive.',
69 + '%1$s %2$s moved to archive.',
70 + $count,
71 + 'propertyhive'
72 + ),
73 + number_format_i18n( $count ),
74 + $count === 1
75 + ? $post_type_object->labels->singular_name
76 + : $post_type_object->labels->name
77 + );
78 +
79 + printf(
80 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
81 + esc_html( $message )
82 + );
83 + }
58 84 }
59 85 }
60 86
87 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
61 88 if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) )
62 89 {
63 - $post_type = isset($_GET['post_type']) ? $_GET['post_type'] : '';
90 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
91 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
64 92 if ( $post_type )
65 93 {
66 94 $post_type_object = get_post_type_object($post_type);
95 + if ( ! $post_type_object ) {
96 + return;
97 + }
67 98
68 - $count = intval($_GET['bulk_unarchived_posts']);
99 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
100 + $count = is_string($_GET['bulk_unarchived_posts']) ? absint($_GET['bulk_unarchived_posts']) : 0;
69 101
70 - printf(
71 - '<div id="message" class="notice is-dismissible updated"><p>' . _n('%s ' . $post_type_object->labels->singular_name . ' Removed from Archive.', '%s ' . $post_type_object->labels->name . ' removed from Archive.', $count, 'propertyhive') . '</p></div>',
72 - $count
73 - );
102 + if ( $post_type_object )
103 + {
104 + $message = sprintf(
105 + /* translators: 1: number of items, 2: post type label */
106 + _n(
107 + '%1$s %2$s removed from archive.',
108 + '%1$s %2$s removed from archive.',
109 + $count,
110 + 'propertyhive'
111 + ),
112 + number_format_i18n( $count ),
113 + $count === 1
114 + ? $post_type_object->labels->singular_name
115 + : $post_type_object->labels->name
116 + );
117 +
118 + printf(
119 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
120 + esc_html( $message )
121 + );
122 + }
74 123 }
75 124 }
76 125 }
77 126
78 - public function retired_template_assistant_admin_notices()
79 - {
80 - if ( is_multisite() )
81 - {
82 - $show = (bool)get_site_option( 'propertyhive_template_assistant_retired_notice', 0 );
83 - if ( !$show ) return;
84 - if ( !is_super_admin() ) return;
85 - }
86 - else
87 - {
88 - $show = (bool)get_option( 'propertyhive_template_assistant_retired_notice', 0 );
89 - if ( !$show ) return;
90 - if ( !current_user_can( 'activate_plugins' ) ) return;
91 - }
92 -
93 - echo '<div class="notice notice-info is-dismissible"><p>
94 - <strong>' . __('The Template Assistant add-on has been retired.', 'propertyhive' ) . '</strong><br>
95 - ' . __('Its functionality is now built into Property Hive under \'Property Hive &gt; Settings &gt; Frontend\'.', 'propertyhive' ) . '<br>
96 - ' . __('The add-on has been deactivated to prevent conflicts. No settings were lost.' ) . '
97 - </p>
98 - <p>
99 - <a href="https://wp-property-hive.com/template-assistant-is-now-part-of-property-hive-core-plugin" target="_blank" class="button button-primary">' . __('Read more', 'propertyhive' ) . '</a>
100 - <a href="" class="button" id="ph_dismiss_notice_retired_template_assistant">' . __('Dismiss this notice', 'propertyhive' ) . '</a>
101 - </p>
102 - </div>';
103 - }
104 -
105 127 public function crm_only_mode_screen_id( $screen_ids )
106 128 {
107 129 $current_user = wp_get_current_user();
108 130
@@ -119,14 +141,20 @@
119 141 }
120 142
121 143 public function check_install_add_on()
122 144 {
123 - if (
124 - isset($_GET['ph_action']) && $_GET['ph_action'] == 'install_add_on' &&
125 - isset($_GET['ph_add_on_slug']) && !empty($_GET['ph_add_on_slug']) &&
126 - isset($_GET['ph_add_on_plugin']) && !empty($_GET['ph_add_on_plugin'])
127 - )
145 + $request_get = wp_unslash( $_GET );
146 + $ph_action = isset( $request_get['ph_action'] ) && is_string( $request_get['ph_action'] ) ? sanitize_key( $request_get['ph_action'] ) : '';
147 + $encoded_slug = isset( $request_get['ph_add_on_slug'] ) && is_string( $request_get['ph_add_on_slug'] ) ? sanitize_text_field( $request_get['ph_add_on_slug'] ) : '';
148 + $encoded_plugin = isset( $request_get['ph_add_on_plugin'] ) && is_string( $request_get['ph_add_on_plugin'] ) ? sanitize_text_field( $request_get['ph_add_on_plugin'] ) : '';
149 +
150 + if ( 'install_add_on' === $ph_action && '' !== $encoded_slug && '' !== $encoded_plugin )
128 151 {
152 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) ) {
153 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
154 + }
155 + check_admin_referer( 'propertyhive-install-add-on' );
156 +
129 157 $installed_plugins = get_option( 'propertyhive_pre_pro_add_ons', array());
130 158
131 159 if ( empty($installed_plugins) )
132 160 {
@@ -132,16 +160,22 @@
132 160 {
133 161 $installed_plugins = array();
134 162 }
135 163
164 + $decoded_slug = base64_decode( $encoded_slug, true );
165 + $decoded_plugin = base64_decode( $encoded_plugin, true );
166 + if ( false === $decoded_slug || false === $decoded_plugin ) {
167 + wp_die( esc_html__( 'Invalid add-on request.', 'propertyhive' ), '', array( 'response' => 400 ) );
168 + }
169 +
136 170 $installed_plugins[] = array(
137 - 'slug' => ph_clean(base64_decode($_GET['ph_add_on_slug'])),
138 - 'plugin' => ph_clean(base64_decode($_GET['ph_add_on_plugin']))
171 + 'slug' => ph_clean( $decoded_slug ),
172 + 'plugin' => ph_clean( $decoded_plugin )
139 173 );
140 174
141 175 update_option( 'propertyhive_pre_pro_add_ons', $installed_plugins );
142 176
143 - wp_redirect( admin_url('admin.php?page=ph-settings&tab=features') );
177 + wp_safe_redirect( admin_url('admin.php?page=ph-settings&tab=features') );
144 178 die();
145 179 }
146 180 }
147 181
@@ -146,12 +180,21 @@
146 180 }
147 181
148 182 public function check_hide_demo_data_tab()
149 183 {
150 - if ( isset($_GET['tab']) && $_GET['tab'] == 'demo_data' && isset($_GET['hidetab']) )
184 + $request_get = wp_unslash( $_GET );
185 + $tab = isset( $request_get['tab'] ) && is_string( $request_get['tab'] ) ? sanitize_key( $request_get['tab'] ) : '';
186 + $hide_tab = isset( $request_get['hidetab'] ) && is_scalar( $request_get['hidetab'] ) ? (string) $request_get['hidetab'] : '';
187 +
188 + if ( 'demo_data' === $tab && '' !== $hide_tab )
151 189 {
190 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
191 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
192 + }
193 + check_admin_referer( 'propertyhive-hide-demo-data' );
194 +
152 195 update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
153 - wp_redirect( admin_url('admin.php?page=ph-settings') );
196 + wp_safe_redirect( admin_url('admin.php?page=ph-settings') );
154 197 die();
155 198 }
156 199 }
157 200
@@ -156,12 +199,40 @@
156 199 }
157 200
158 201 public function export_sub_grid()
159 202 {
160 - if (
161 - isset($_GET['sub_grid']) && !empty(ph_clean($_GET['sub_grid']))
162 - )
203 + $request_get = wp_unslash( $_GET );
204 + $sub_grid = isset( $request_get['sub_grid'] ) && is_string( $request_get['sub_grid'] ) ? sanitize_key( $request_get['sub_grid'] ) : '';
205 + $raw_record_ids = isset( $request_get['record_ids'] ) && is_string( $request_get['record_ids'] ) ? sanitize_text_field( $request_get['record_ids'] ) : '';
206 +
207 + if ( '' !== $sub_grid )
163 208 {
209 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
210 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
211 + }
212 + check_admin_referer( 'propertyhive-export-sub-grid', 'ph_export_nonce' );
213 +
214 + $export_types = array(
215 + 'property-viewings-grid' => 'viewing',
216 + 'contact-viewings-grid' => 'viewing',
217 + 'property-offers-grid' => 'offer',
218 + 'contact-offers-grid' => 'offer',
219 + 'property-sales-grid' => 'sale',
220 + 'contact-sales-grid' => 'sale',
221 + );
222 + $record_ids = '' !== $raw_record_ids
223 + ? array_values( array_filter( array_map( 'absint', explode( '|', $raw_record_ids ) ) ) )
224 + : array();
225 +
226 + if ( ! isset( $export_types[ $sub_grid ] ) || empty( $record_ids ) ) {
227 + wp_die( esc_html__( 'Invalid export request', 'propertyhive' ), '', array( 'response' => 400 ) );
228 + }
229 + foreach ( $record_ids as $record_id ) {
230 + if ( get_post_type( $record_id ) !== $export_types[ $sub_grid ] || ! current_user_can( 'edit_post', $record_id ) ) {
231 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
232 + }
233 + }
234 +
164 235 ob_start();
165 236
166 237 $df = fopen("php://output", 'w');
167 238
@@ -166,9 +237,9 @@
166 237 $df = fopen("php://output", 'w');
167 238
168 239 $columns = array( 'id' => __( 'ID', 'propertyhive' ) );
169 240
170 - if ( strpos(ph_clean($_GET['sub_grid']), 'viewings') )
241 + if ( strpos( $sub_grid, 'viewings' ) !== false )
171 242 {
172 243 $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
173 244 $columns['property'] = __( 'Property', 'propertyhive' );
174 245 //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
@@ -176,9 +247,9 @@
176 247 $columns['negotiator'] = __( 'Attending Negotiator(s)', 'propertyhive' );
177 248 $columns['status'] = __( 'Status', 'propertyhive' );
178 249 $columns['feedback'] = __( 'Feedback', 'propertyhive' );
179 250 }
180 - elseif ( strpos(ph_clean($_GET['sub_grid']), 'offers') )
251 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
181 252 {
182 253 $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
183 254 $columns['property'] = __( 'Property', 'propertyhive' );
184 255 //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
@@ -185,9 +256,9 @@
185 256 $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
186 257 $columns['status'] = __( 'Status', 'propertyhive' );
187 258 $columns['amount'] = __( 'Offer Amount', 'propertyhive' );
188 259 }
189 - elseif ( strpos(ph_clean($_GET['sub_grid']), 'sales') )
260 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
190 261 {
191 262 $columns['date'] = __( 'Date', 'propertyhive' );
192 263 $columns['property'] = __( 'Property', 'propertyhive' );
193 264 //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
@@ -197,15 +268,13 @@
197 268 }
198 269
199 270 fputcsv($df, $columns);
200 271
201 - if ( isset($_GET['record_ids']) && !empty(ph_clean($_GET['record_ids'])) )
272 + if ( ! empty( $record_ids ) )
202 273 {
203 - $record_ids = explode("|", ph_clean($_GET['record_ids']));
204 -
205 274 if ( !empty($record_ids) )
206 275 {
207 - if ( strpos(ph_clean($_GET['sub_grid']), 'viewings') )
276 + if ( strpos( $sub_grid, 'viewings' ) !== false )
208 277 {
209 278 $args = array(
210 279 'post_type' => 'viewing',
211 280 'nopaging' => TRUE,
@@ -212,8 +281,9 @@
212 281 'fields' => 'ids',
213 282 'post__in' => $record_ids,
214 283 'order' => 'ASC',
215 284 'orderby' => 'meta_value',
285 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked viewing list by its fixed date-time metadata key.
216 286 'meta_key' => '_start_date_time',
217 287 );
218 288
219 289 $records_query = new WP_Query( $args );
@@ -235,9 +305,9 @@
235 305 }
236 306
237 307 $columns = array(
238 308 get_the_ID(),
239 - date("H:i jS F Y", strtotime($viewing->_start_date_time)),
309 + gmdate("H:i jS F Y", strtotime($viewing->_start_date_time)),
240 310 $property_address,
241 311 str_replace("<br>", "\n", $viewing->get_applicants()),
242 312 $viewing->get_negotiators(),
243 313 str_replace("<br>", "\n", $viewing->get_status()),
@@ -247,9 +317,9 @@
247 317 fputcsv($df, $columns);
248 318 }
249 319 }
250 320 }
251 - elseif ( strpos(ph_clean($_GET['sub_grid']), 'offers') )
321 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
252 322 {
253 323 $args = array(
254 324 'post_type' => 'offer',
255 325 'nopaging' => TRUE,
@@ -256,8 +326,9 @@
256 326 'fields' => 'ids',
257 327 'post__in' => $record_ids,
258 328 'order' => 'ASC',
259 329 'orderby' => 'meta_value',
330 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked offer list by its fixed date-time metadata key.
260 331 'meta_key' => '_offer_date_time',
261 332 );
262 333
263 334 $records_query = new WP_Query( $args );
@@ -279,9 +350,9 @@
279 350 }
280 351
281 352 $columns = array(
282 353 get_the_ID(),
283 - date("H:i jS F Y", strtotime($offer->_offer_date_time)),
354 + gmdate("H:i jS F Y", strtotime($offer->_offer_date_time)),
284 355 $property_address,
285 356 str_replace("<br>", "\n", $offer->get_applicants()),
286 357 $offer->_status,
287 358 html_entity_decode($offer->get_formatted_amount())
@@ -290,9 +361,9 @@
290 361 fputcsv($df, $columns);
291 362 }
292 363 }
293 364 }
294 - elseif ( strpos(ph_clean($_GET['sub_grid']), 'sales') )
365 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
295 366 {
296 367 $args = array(
297 368 'post_type' => 'sale',
298 369 'nopaging' => TRUE,
@@ -299,8 +370,9 @@
299 370 'fields' => 'ids',
300 371 'post__in' => $record_ids,
301 372 'order' => 'ASC',
302 373 'orderby' => 'meta_value',
374 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked sale list by its fixed date-time metadata key.
303 375 'meta_key' => '_sale_date_time',
304 376 );
305 377
306 378 $records_query = new WP_Query( $args );
@@ -322,9 +394,9 @@
322 394 }
323 395
324 396 $columns = array(
325 397 get_the_ID(),
326 - date("jS F Y", strtotime($sale->_sale_date_time)),
398 + gmdate("jS F Y", strtotime($sale->_sale_date_time)),
327 399 $property_address,
328 400 str_replace("<br>", "\n", $sale->get_applicants()),
329 401 $sale->_status,
330 402 html_entity_decode($sale->get_formatted_amount())
@@ -336,13 +408,13 @@
336 408 }
337 409 }
338 410 }
339 411
340 - fclose($df);
412 + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
341 413
342 414 $output = ob_get_clean();
343 415
344 - $filename = sanitize_title(ph_clean($_GET['sub_grid'])) . '-' . date("YmdHis") . '.csv';
416 + $filename = sanitize_title( $sub_grid ) . '-' . gmdate("YmdHis") . '.csv';
345 417
346 418 // disable caching
347 419 $now = gmdate("D, d M Y H:i:s");
348 420 header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
@@ -357,8 +429,9 @@
357 429 // disposition / encoding on response body
358 430 header("Content-Disposition: attachment;filename={$filename}");
359 431 header("Content-Transfer-Encoding: binary");
360 432
433 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download produced by fputcsv, not HTML; HTML escaping would corrupt exported field values.
361 434 echo $output;
362 435
363 436 die();
364 437 }
@@ -365,12 +438,16 @@
365 438 }
366 439
367 440 public function export_applicant_list()
368 441 {
369 - if (
370 - isset($_POST['submitted_applicant_list']) && $_POST['submitted_applicant_list'] == '1' &&
371 - isset($_POST['export_applicant_list_results']) && $_POST['export_applicant_list_results'] == '1'
372 - )
442 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
443 + $request_post = wp_unslash( $_POST );
444 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
445 + $submitted_applicant_list = isset( $request_post['submitted_applicant_list'] ) && '1' === (string) $request_post['submitted_applicant_list'];
446 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
447 + $export_applicant_list_results = isset( $request_post['export_applicant_list_results'] ) && '1' === (string) $request_post['export_applicant_list_results'];
448 +
449 + if ( $submitted_applicant_list && $export_applicant_list_results )
373 450 {
374 451 include_once( 'class-ph-admin-applicant-list.php' );
375 452 $ph_admin_applicant_list = new PH_Admin_Applicant_List();
376 453 $ph_admin_applicant_list->export();
@@ -380,13 +457,18 @@
380 457 public function record_recently_viewed()
381 458 {
382 459 global $pagenow;
383 460
461 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
462 + $request_get = wp_unslash( $_GET );
463 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
464 + $recent_post_id = isset( $request_get['post'] ) && is_scalar( $request_get['post'] ) ? absint( $request_get['post'] ) : 0;
465 +
384 466 if (
385 467 'post.php' === $pagenow &&
386 - isset($_GET['post']) &&
468 + $recent_post_id > 0 &&
387 469 in_array(
388 - get_post_type((int)$_GET['post']),
470 + get_post_type( $recent_post_id ),
389 471 apply_filters( 'propertyhive_post_types_with_tabs', array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale') )
390 472 )
391 473 )
392 474 {
@@ -398,27 +480,27 @@
398 480 }
399 481
400 482 foreach ( $recently_viewed as $time => $post )
401 483 {
402 - if ( (int)$_GET['post'] == $post['id'] )
484 + if ( $recent_post_id == $post['id'] )
403 485 {
404 486 unset($recently_viewed[$time]);
405 487 }
406 488 }
407 489
408 - $title = get_the_title((int)$_GET['post']);
490 + $title = get_the_title( $recent_post_id );
409 491
410 - switch ( get_post_type((int)$_GET['post']) )
492 + switch ( get_post_type( $recent_post_id ) )
411 493 {
412 494 case "appraisal":
413 495 {
414 - $appraisal = new PH_Appraisal( (int)$_GET['post'] );
496 + $appraisal = new PH_Appraisal( $recent_post_id );
415 497 $title = $appraisal->get_formatted_summary_address();
416 498 break;
417 499 }
418 500 case "property":
419 501 {
420 - $property = new PH_Property( (int)$_GET['post'] );
502 + $property = new PH_Property( $recent_post_id );
421 503 $title = $property->get_formatted_summary_address();
422 504 break;
423 505 }
424 506 case "enquiry":
@@ -425,9 +507,9 @@
425 507 case "viewing":
426 508 case "offer":
427 509 case "sale":
428 510 {
429 - $property_id = get_post_meta( (int)$_GET['post'], '_property_id', TRUE );
511 + $property_id = get_post_meta( $recent_post_id, '_property_id', TRUE );
430 512 if ( $property_id != '' )
431 513 {
432 514 $property = new PH_Property( (int)$property_id );
433 515 $title = $property->get_formatted_summary_address();
@@ -435,15 +517,15 @@
435 517 break;
436 518 }
437 519 }
438 520
439 - $title = ucfirst(get_post_type((int)$_GET['post'])) . ' - ' . $title;
521 + $title = ucfirst( get_post_type( $recent_post_id ) ) . ' - ' . $title;
440 522
441 523 $recently_viewed = array(time() => array(
442 - 'id' => (int)$_GET['post'],
524 + 'id' => $recent_post_id,
443 525 'title' => $title,
444 - 'post_type' => get_post_type((int)$_GET['post']),
445 - 'edit_link' => get_edit_post_link((int)$_GET['post']),
526 + 'post_type' => get_post_type( $recent_post_id ),
527 + 'edit_link' => get_edit_post_link( $recent_post_id ),
446 528 )) + $recently_viewed;
447 529
448 530 $recently_viewed = array_slice($recently_viewed, 0, 10, TRUE);
449 531
@@ -452,11 +534,15 @@
452 534 }
453 535
454 536 public function admin_dashboard_pages()
455 537 {
456 - if ( ! empty( $_GET['page'] ) )
538 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This registers a read-only admin dashboard page and does not change state.
539 + $request_get = wp_unslash( $_GET );
540 + $admin_page = isset( $request_get['page'] ) && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
541 +
542 + if ( '' !== $admin_page )
457 543 {
458 - switch ( sanitize_title($_GET['page']) )
544 + switch ( $admin_page )
459 545 {
460 546 case 'ph-installed':
461 547 {
462 548 add_dashboard_page(
@@ -462,9 +548,9 @@
462 548 add_dashboard_page(
463 549 __( 'Welcome to Property Hive', 'propertyhive' ),
464 550 __( 'Welcome to Property Hive', 'propertyhive' ),
465 551 'manage_propertyhive',
466 - sanitize_title($_GET['page']),
552 + $admin_page,
467 553 array( $this, 'installed_screen' )
468 554 );
469 555
470 556 break;
@@ -545,9 +631,9 @@
545 631
546 632 <a href="https://wp-property-hive.com/honeycomb" target="_blank"><img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/honeycomb-screenshot.png" style="margin:0 auto; display:block; max-width:80%;" alt="Property Hive Free Honeycomb Theme"></a>
547 633
548 634 <p><strong style="font-size:14px;">Leave a Review</strong><br>
549 - If you've found Property Hive useful we'd love it if you could spare a moment to tell others just how great we are by <a href="https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5" target="_blank">leaving a review</a>.</p>
635 + If you've found Property Hive useful we'd love it if you could spare a moment to tell others just how great we are by <a href="https://wordpress.org/support/plugin/propertyhive/reviews/" target="_blank">leaving a review</a>.</p>
550 636
551 637 <p><strong style="font-size:14px;">Contribute</strong><br>
552 638 Property Hive is completely open-source meaning anyone can access and contribute to the code. Fixing bugs and adding functionality can be done by anyone with coding knowledge. <a href="https://github.com/propertyhive/WP-Property-Hive" target="_blank">Visit us on GitHub</a> to get started.</p>
553 639
@@ -584,9 +670,10 @@
584 670 include_once( 'ph-meta-box-functions.php' );
585 671
586 672 // Classes
587 673 include_once( 'class-ph-admin-post-types.php' );
588 - //include_once( 'class-ph-admin-taxonomies.php' );
674 + include_once( 'class-ph-admin-onboarding.php' );
675 + include_once( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
589 676
590 677 // Classes we only need if the ajax is not-ajax
591 678 if ( ! is_ajax() ) {
592 679 include( 'class-ph-admin-menus.php' );
@@ -655,8 +742,17 @@
655 742 public function review_admin_notices()
656 743 {
657 744 global $wpdb;
658 745
746 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This method only renders read-only admin notices.
747 + $request_get = wp_unslash( $_GET );
748 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This method only checks whether a settings POST is present to suppress a duplicate read-only notice; it does not process or save the value.
749 + $request_post = wp_unslash( $_POST );
750 + $admin_page_present = isset( $request_get['page'] );
751 + $admin_page = $admin_page_present && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
752 + $plugin_status_present = isset( $request_get['plugin_status'] );
753 + $maps_api_key_submitted = isset( $request_post['propertyhive_google_maps_api_key'] );
754 +
659 755 if ( current_user_can( 'manage_options' ) )
660 756 {
661 757 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
662 758 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -664,12 +760,12 @@
664 760 if ( $propertyhive_review_prompt_due_timestamp < time() )
665 761 {
666 762 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
667 763 <p>
668 - " . __( '<strong>Finding Property Hive useful?</strong> Please take a minute to <a href="https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post" target="_blank">leave us a ★★★★★ review</a>', 'propertyhive' ) . "
764 + " . wp_kses_post( __( '<strong>Finding Property Hive useful?</strong> Please take a minute to <a href="https://wordpress.org/support/plugin/propertyhive/reviews/#new-post" target="_blank">leave us a review</a>', 'propertyhive' ) ) . "
669 765 </p>
670 766 <p>
671 - <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
767 + <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
672 768 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
673 769 </p>
674 770 </div>";
675 771 }
@@ -676,15 +772,15 @@
676 772 }
677 773
678 774 if (
679 775 class_exists('Easy_Property_Listings') &&
680 - !isset($_GET['plugin_status']) &&
776 + ! $plugin_status_present &&
681 777 get_option( 'epl_notice_dismissed', '' ) != 'yes'
682 778 )
683 779 {
684 780 echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\">
685 781 <p>
686 - " . __( '<strong>It looks like you\'re also running Easy Property Listings.</strong> This will cause conflicts with Property Hive and should be deactivated.', 'propertyhive' ) . "
782 + " . wp_kses_post( __( '<strong>It looks like you\'re also running Easy Property Listings.</strong> This will cause conflicts with Property Hive and should be deactivated.', 'propertyhive' ) ) . "
687 783 </p>
688 784 <p>
689 785 <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a>
690 786 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a>
@@ -697,12 +793,12 @@
697 793 !class_exists('PH_Demo_Data') &&
698 794 get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 &&
699 795 get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' &&
700 796 (
701 - !isset($_GET['page'])
797 + ! $admin_page_present
702 798 ||
703 799 (
704 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
800 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
705 801 )
706 802 )
707 803 )
708 804 {
@@ -707,9 +803,9 @@
707 803 )
708 804 {
709 805 echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\">
710 806 <p>
711 - " . __( '<strong>New To Property Hive?</strong> Did you know that you can quickly import demo data to get a feel for how Property Hive works?', 'propertyhive' ) . "
807 + " . wp_kses_post( __( '<strong>New To Property Hive?</strong> Did you know that you can quickly import demo data to get a feel for how Property Hive works?', 'propertyhive' ) ) . "
712 808 </p>
713 809 <p>
714 810 <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a>
715 811 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a>
@@ -720,12 +816,12 @@
720 816
721 817 if (
722 818 get_option('propertyhive_search_results_page_id', '') == '' &&
723 819 (
724 - !isset($_GET['page'])
820 + ! $admin_page_present
725 821 ||
726 822 (
727 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
823 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
728 824 )
729 825 ) &&
730 826 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
731 827 )
@@ -731,9 +827,9 @@
731 827 )
732 828 {
733 829 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
734 830 <p>
735 - " . __( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
831 + " . esc_html__( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
736 832 </p>
737 833 <p>
738 834 <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To Property Hive Settings', 'propertyhive' )) . "</a>
739 835 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
@@ -743,15 +839,16 @@
743 839 }
744 840
745 841 if (
746 842 get_option('propertyhive_maps_provider') !== 'osm' &&
843 + get_option('propertyhive_maps_provider') !== 'mapbox' &&
747 844 get_option('propertyhive_google_maps_api_key', '') == '' &&
748 - !isset($_POST['propertyhive_google_maps_api_key']) &&
845 + ! $maps_api_key_submitted &&
749 846 (
750 - !isset($_GET['page'])
847 + ! $admin_page_present
751 848 ||
752 849 (
753 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
850 + $admin_page_present && 'ph-installed' !== $admin_page
754 851 )
755 852 ) &&
756 853 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
757 854 )
@@ -757,9 +854,13 @@
757 854 )
758 855 {
759 856 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
760 857 <p>
761 - " . sprintf( __( 'We noticed that you haven\'t entered a Google Maps API key yet. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="%s">enter it</a>.', 'propertyhive' ), admin_url('admin.php?page=ph-settings&tab=general&section=map') ) . "
858 + " . sprintf(
859 + /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
860 + wp_kses_post( __( 'We noticed that you haven\'t entered a Google Maps API key. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="%s">enter it</a>.', 'propertyhive' ) ),
861 + esc_url( admin_url('admin.php?page=ph-settings&tab=general&section=map') )
862 + ) . "
762 863 </p>
763 864 <p>
764 865 <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general&section=map')) . "\" class=\"button-primary\">" . esc_html(__( 'Enter Google Maps API Key', 'propertyhive' )) . "</a>
765 866 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
@@ -771,12 +872,12 @@
771 872 if (
772 873 get_option('propertyhive_license_key', '') != '' &&
773 874 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
774 875 (
775 - !isset($_GET['page'])
876 + ! $admin_page_present
776 877 ||
777 878 (
778 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
879 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
779 880 )
780 881 )
781 882 )
782 883 {
@@ -810,8 +911,9 @@
810 911 $screen = get_current_screen();
811 912 if ( in_array( $screen->id, array( 'dashboard' ) ) )
812 913 {
813 914 // Email Cron Warning
915 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table; this read-only dashboard notice has no WordPress API equivalent.
814 916 $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
815 917 $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
816 918 if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
817 919 {
@@ -827,9 +929,9 @@
827 929 }
828 930 }
829 931 }
830 932
831 - if ( isset($_GET['propertyhive_contacts_merged']) )
933 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
832 934 {
833 935 echo '
834 936 <div class="notice notice-info">
835 937 <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
@@ -848,13 +950,14 @@
848 950 {
849 951 delete_transient( '_ph_activation_redirect' );
850 952
851 953 // Don't do redirect if part of multisite, doing batch-activate, or if no permission
852 - if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
954 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
955 + if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_options' ) ) {
853 956 return;
854 957 }
855 958
856 - wp_safe_redirect( admin_url( 'index.php?page=ph-installed' ) );
959 + wp_safe_redirect( admin_url( 'index.php?page=ph-onboarding' ) );
857 960 exit;
858 961 }
859 962 }
860 963
@@ -867,9 +970,10 @@
867 970
868 971 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
869 972 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
870 973 {
871 - exit( wp_redirect( home_url( '/' ) ) );
974 + wp_safe_redirect( home_url( '/' ) );
975 + exit;
872 976 }
873 977 }
874 978
875 979 /**
@@ -882,16 +986,26 @@
882 986 global $wpdb;
883 987
884 988 if ( isset( $_GET['view_propertyhive_email'] ) )
885 989 {
886 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'view-email' ) )
990 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
991 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
992 + }
993 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'view-email' ) )
887 994 {
888 - die( 'Security check' );
995 + wp_die( 'Security check' );
889 996 }
890 997
998 + if ( ! current_user_can( 'manage_propertyhive' ) )
999 + {
1000 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1001 + }
1002 +
891 1003 if ( isset( $_GET['email_id'] ) )
892 1004 {
893 - $email_log = $wpdb->get_row( "SELECT * FROM " . $wpdb->prefix . "ph_email_log WHERE email_id = '" . esc_sql( (int)$_GET['email_id'] ) . "'" );
1005 + $email_id = is_string( $_GET['email_id'] ) ? absint( $_GET['email_id'] ) : 0;
1006 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Email logs are stored in a custom plugin table and this is a single protected administrative lookup.
1007 + $email_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ph_email_log WHERE email_id = %d", $email_id ) );
894 1008 if ( null !== $email_log )
895 1009 {
896 1010 $body = $email_log->body;
897 1011
@@ -899,9 +1013,12 @@
899 1013 {
900 1014 $body = gzuncompress($body);
901 1015 }
902 1016
903 - echo apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1017 + $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1018 +
1019 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is the rendered HTML email viewer. The body was sanitized before entering the email log; propertyhive_mail_content and email templates are intentional trusted HTML extension points.
1020 + echo $message;
904 1021
905 1022 }
906 1023 else
907 1024 {
@@ -920,23 +1037,29 @@
920 1037 */
921 1038 public function preview_emails() {
922 1039 if ( isset( $_GET['preview_propertyhive_email'] ) )
923 1040 {
924 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-applicants' ) )
1041 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
1042 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
1043 + }
1044 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-properties' ) && ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-applicants' ) )
925 1045 {
926 1046 die( 'Security check' );
927 1047 }
928 1048
929 1049 $current_user = wp_get_current_user();
1050 + $request_get = wp_unslash( $_GET );
1051 + $request_post = wp_unslash( $_POST );
930 1052
931 1053 // get the preview email content
932 - if ( isset($_GET['property_id']) )
1054 + $email_property_ids = array();
1055 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
933 1056 {
934 - $email_property_ids = array((int)$_GET['property_id']);
1057 + $email_property_ids = array( absint( $request_get['property_id'] ) );
935 1058 }
936 - elseif ( isset($_POST['email_property_id']) )
1059 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
937 1060 {
938 - $email_property_ids = explode(",", sanitize_text_field($_POST['email_property_id']));
1061 + $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
939 1062 }
940 1063
941 1064 $allowed_tags = array(
942 1065 'strong' => array(),
@@ -957,17 +1080,18 @@
957 1080 ),
958 1081 );
959 1082 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
960 1083
961 - $body = wp_kses(wp_unslash($_POST['body']), $allowedposttags);
1084 + $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1085 + $body = wp_kses( $raw_body, $allowed_tags );
962 1086
963 - if ( isset($_GET['contact_id']) )
1087 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
964 1088 {
965 - $contact = new PH_Contact((int)$_GET['contact_id']);
966 - $body = str_replace("[contact_name]", $contact->post_title, $body);
967 - $body = str_replace("[contact_dear]", $contact->dear(), $body);
1089 + $contact = new PH_Contact( absint( $request_get['contact_id'] ) );
1090 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $body );
1091 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
968 1092 }
969 - $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
1093 + $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body );
970 1094
971 1095 $office_counts = array();
972 1096
973 1097 if ( strpos($body, '[properties]') !== FALSE )
@@ -1008,22 +1132,23 @@
1008 1132 }
1009 1133
1010 1134 if ( !empty($office_id) )
1011 1135 {
1012 - $office_name = get_the_title($office_id);
1013 - $office_email_address = get_post_meta( $office_id, '_office_email_address_sales', TRUE );
1136 + $office_name = get_the_title( (int) $office_id );
1137 + $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE );
1014 1138 }
1015 1139
1016 - $body = str_replace("[office_name]", $office_name, $body);
1017 - $body = str_replace("[office_email_address]", $office_email_address, $body);
1140 + $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
1141 + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
1018 1142
1019 - $body = str_replace("[negotiator_name]", $current_user->display_name, $body);
1020 - $body = str_replace("[negotiator_email_address]", $current_user->user_email, $body);
1143 + $body = str_replace( '[negotiator_name]', esc_html( $current_user->display_name ), $body );
1144 + $body = str_replace( '[negotiator_email_address]', esc_html( $current_user->user_email ), $body );
1021 1145
1022 1146 // wrap the content with the email template and then add styles
1023 1147 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1024 1148
1025 1149 // print the preview email
1150 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is the rendered HTML email preview. The request body was passed through the explicit match allowlist; templates and propertyhive_mail_content are intentional trusted HTML extension points.
1026 1151 echo $message;
1027 1152 exit;
1028 1153 }
1029 1154 }