PluginProbe
Property Hive / 2.3.1
Property Hive v2.3.1
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 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin.php +251 -127 2.2.22.3.1 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,9 @@
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( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
589 675
590 676 // Classes we only need if the ajax is not-ajax
591 677 if ( ! is_ajax() ) {
592 678 include( 'class-ph-admin-menus.php' );
@@ -655,8 +741,17 @@
655 741 public function review_admin_notices()
656 742 {
657 743 global $wpdb;
658 744
745 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This method only renders read-only admin notices.
746 + $request_get = wp_unslash( $_GET );
747 + // 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.
748 + $request_post = wp_unslash( $_POST );
749 + $admin_page_present = isset( $request_get['page'] );
750 + $admin_page = $admin_page_present && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
751 + $plugin_status_present = isset( $request_get['plugin_status'] );
752 + $maps_api_key_submitted = isset( $request_post['propertyhive_google_maps_api_key'] );
753 +
659 754 if ( current_user_can( 'manage_options' ) )
660 755 {
661 756 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
662 757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -664,12 +759,12 @@
664 759 if ( $propertyhive_review_prompt_due_timestamp < time() )
665 760 {
666 761 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
667 762 <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' ) . "
763 + " . 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 764 </p>
670 765 <p>
671 - <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
766 + <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
672 767 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
673 768 </p>
674 769 </div>";
675 770 }
@@ -676,15 +771,15 @@
676 771 }
677 772
678 773 if (
679 774 class_exists('Easy_Property_Listings') &&
680 - !isset($_GET['plugin_status']) &&
775 + ! $plugin_status_present &&
681 776 get_option( 'epl_notice_dismissed', '' ) != 'yes'
682 777 )
683 778 {
684 779 echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\">
685 780 <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' ) . "
781 + " . 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 782 </p>
688 783 <p>
689 784 <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a>
690 785 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a>
@@ -697,12 +792,12 @@
697 792 !class_exists('PH_Demo_Data') &&
698 793 get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 &&
699 794 get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' &&
700 795 (
701 - !isset($_GET['page'])
796 + ! $admin_page_present
702 797 ||
703 798 (
704 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
799 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
705 800 )
706 801 )
707 802 )
708 803 {
@@ -707,9 +802,9 @@
707 802 )
708 803 {
709 804 echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\">
710 805 <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' ) . "
806 + " . 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 807 </p>
713 808 <p>
714 809 <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a>
715 810 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a>
@@ -720,12 +815,12 @@
720 815
721 816 if (
722 817 get_option('propertyhive_search_results_page_id', '') == '' &&
723 818 (
724 - !isset($_GET['page'])
819 + ! $admin_page_present
725 820 ||
726 821 (
727 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
822 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
728 823 )
729 824 ) &&
730 825 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
731 826 )
@@ -731,9 +826,9 @@
731 826 )
732 827 {
733 828 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
734 829 <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' ) . "
830 + " . 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 831 </p>
737 832 <p>
738 833 <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 834 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
@@ -743,15 +838,16 @@
743 838 }
744 839
745 840 if (
746 841 get_option('propertyhive_maps_provider') !== 'osm' &&
842 + get_option('propertyhive_maps_provider') !== 'mapbox' &&
747 843 get_option('propertyhive_google_maps_api_key', '') == '' &&
748 - !isset($_POST['propertyhive_google_maps_api_key']) &&
844 + ! $maps_api_key_submitted &&
749 845 (
750 - !isset($_GET['page'])
846 + ! $admin_page_present
751 847 ||
752 848 (
753 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
849 + $admin_page_present && 'ph-installed' !== $admin_page
754 850 )
755 851 ) &&
756 852 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
757 853 )
@@ -757,9 +853,13 @@
757 853 )
758 854 {
759 855 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
760 856 <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') ) . "
857 + " . sprintf(
858 + /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
859 + 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' ) ),
860 + esc_url( admin_url('admin.php?page=ph-settings&tab=general&section=map') )
861 + ) . "
762 862 </p>
763 863 <p>
764 864 <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 865 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
@@ -771,12 +871,12 @@
771 871 if (
772 872 get_option('propertyhive_license_key', '') != '' &&
773 873 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
774 874 (
775 - !isset($_GET['page'])
875 + ! $admin_page_present
776 876 ||
777 877 (
778 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
878 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
779 879 )
780 880 )
781 881 )
782 882 {
@@ -810,8 +910,9 @@
810 910 $screen = get_current_screen();
811 911 if ( in_array( $screen->id, array( 'dashboard' ) ) )
812 912 {
813 913 // Email Cron Warning
914 + // 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 915 $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
815 916 $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
816 917 if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
817 918 {
@@ -827,9 +928,9 @@
827 928 }
828 929 }
829 930 }
830 931
831 - if ( isset($_GET['propertyhive_contacts_merged']) )
932 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
832 933 {
833 934 echo '
834 935 <div class="notice notice-info">
835 936 <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
@@ -848,8 +949,9 @@
848 949 {
849 950 delete_transient( '_ph_activation_redirect' );
850 951
851 952 // Don't do redirect if part of multisite, doing batch-activate, or if no permission
953 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
852 954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
853 955 return;
854 956 }
855 957
@@ -867,9 +969,10 @@
867 969
868 970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
869 971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
870 972 {
871 - exit( wp_redirect( home_url( '/' ) ) );
973 + wp_safe_redirect( home_url( '/' ) );
974 + exit;
872 975 }
873 976 }
874 977
875 978 /**
@@ -882,16 +985,26 @@
882 985 global $wpdb;
883 986
884 987 if ( isset( $_GET['view_propertyhive_email'] ) )
885 988 {
886 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'view-email' ) )
989 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
990 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
991 + }
992 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'view-email' ) )
887 993 {
888 - die( 'Security check' );
994 + wp_die( 'Security check' );
889 995 }
890 996
997 + if ( ! current_user_can( 'manage_propertyhive' ) )
998 + {
999 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 + }
1001 +
891 1002 if ( isset( $_GET['email_id'] ) )
892 1003 {
893 - $email_log = $wpdb->get_row( "SELECT * FROM " . $wpdb->prefix . "ph_email_log WHERE email_id = '" . esc_sql( (int)$_GET['email_id'] ) . "'" );
1004 + $email_id = is_string( $_GET['email_id'] ) ? absint( $_GET['email_id'] ) : 0;
1005 + // 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.
1006 + $email_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ph_email_log WHERE email_id = %d", $email_id ) );
894 1007 if ( null !== $email_log )
895 1008 {
896 1009 $body = $email_log->body;
897 1010
@@ -899,9 +1012,12 @@
899 1012 {
900 1013 $body = gzuncompress($body);
901 1014 }
902 1015
903 - echo apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1016 + $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1017 +
1018 + // 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.
1019 + echo $message;
904 1020
905 1021 }
906 1022 else
907 1023 {
@@ -920,23 +1036,29 @@
920 1036 */
921 1037 public function preview_emails() {
922 1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
923 1039 {
924 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-applicants' ) )
1040 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
1041 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
1042 + }
1043 + 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 1044 {
926 1045 die( 'Security check' );
927 1046 }
928 1047
929 1048 $current_user = wp_get_current_user();
1049 + $request_get = wp_unslash( $_GET );
1050 + $request_post = wp_unslash( $_POST );
930 1051
931 1052 // get the preview email content
932 - if ( isset($_GET['property_id']) )
1053 + $email_property_ids = array();
1054 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
933 1055 {
934 - $email_property_ids = array((int)$_GET['property_id']);
1056 + $email_property_ids = array( absint( $request_get['property_id'] ) );
935 1057 }
936 - elseif ( isset($_POST['email_property_id']) )
1058 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
937 1059 {
938 - $email_property_ids = explode(",", sanitize_text_field($_POST['email_property_id']));
1060 + $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
939 1061 }
940 1062
941 1063 $allowed_tags = array(
942 1064 'strong' => array(),
@@ -957,17 +1079,18 @@
957 1079 ),
958 1080 );
959 1081 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
960 1082
961 - $body = wp_kses(wp_unslash($_POST['body']), $allowedposttags);
1083 + $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1084 + $body = wp_kses( $raw_body, $allowed_tags );
962 1085
963 - if ( isset($_GET['contact_id']) )
1086 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
964 1087 {
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);
1088 + $contact = new PH_Contact( absint( $request_get['contact_id'] ) );
1089 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $body );
1090 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
968 1091 }
969 - $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
1092 + $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body );
970 1093
971 1094 $office_counts = array();
972 1095
973 1096 if ( strpos($body, '[properties]') !== FALSE )
@@ -1008,22 +1131,23 @@
1008 1131 }
1009 1132
1010 1133 if ( !empty($office_id) )
1011 1134 {
1012 - $office_name = get_the_title($office_id);
1013 - $office_email_address = get_post_meta( $office_id, '_office_email_address_sales', TRUE );
1135 + $office_name = get_the_title( (int) $office_id );
1136 + $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE );
1014 1137 }
1015 1138
1016 - $body = str_replace("[office_name]", $office_name, $body);
1017 - $body = str_replace("[office_email_address]", $office_email_address, $body);
1139 + $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
1140 + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
1018 1141
1019 - $body = str_replace("[negotiator_name]", $current_user->display_name, $body);
1020 - $body = str_replace("[negotiator_email_address]", $current_user->user_email, $body);
1142 + $body = str_replace( '[negotiator_name]', esc_html( $current_user->display_name ), $body );
1143 + $body = str_replace( '[negotiator_email_address]', esc_html( $current_user->user_email ), $body );
1021 1144
1022 1145 // wrap the content with the email template and then add styles
1023 1146 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1024 1147
1025 1148 // print the preview email
1149 + // 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 1150 echo $message;
1027 1151 exit;
1028 1152 }
1029 1153 }