PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.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 1.4.61 All 261 releases
← All changes | includes/admin/class-ph-admin.php +244 -125 2.2.52.3.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
@@ -40,65 +44,83 @@
40 44 }
41 45
42 46 public function archive_admin_notices()
43 47 {
48 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
44 49 if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts']))
45 50 {
46 - $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']) ) : '';
47 53 if ( $post_type )
48 54 {
49 55 $post_type_object = get_post_type_object($post_type);
56 + if ( ! $post_type_object ) {
57 + return;
58 + }
50 59
51 - $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;
52 62
53 - $message = sprintf(
54 - /* translators: 1: number of items, 2: post type label */
55 - _n(
56 - '%1$s %2$s moved to archive.',
57 - '%1$s %2$s moved to archive.',
58 - $count,
59 - 'propertyhive'
60 - ),
61 - number_format_i18n( $count ),
62 - $count === 1
63 - ? $post_type_object->labels->singular_name
64 - : $post_type_object->labels->name
65 - );
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 + );
66 78
67 - printf(
68 - '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
69 - $message
70 - );
79 + printf(
80 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
81 + esc_html( $message )
82 + );
83 + }
71 84 }
72 85 }
73 86
87 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
74 88 if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) )
75 89 {
76 - $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']) ) : '';
77 92 if ( $post_type )
78 93 {
79 94 $post_type_object = get_post_type_object($post_type);
95 + if ( ! $post_type_object ) {
96 + return;
97 + }
80 98
81 - $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;
82 101
83 - $message = sprintf(
84 - /* translators: 1: number of items, 2: post type label */
85 - _n(
86 - '%1$s %2$s removed from archive.',
87 - '%1$s %2$s removed from archive.',
88 - $count,
89 - 'propertyhive'
90 - ),
91 - number_format_i18n( $count ),
92 - $count === 1
93 - ? $post_type_object->labels->singular_name
94 - : $post_type_object->labels->name
95 - );
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 + );
96 117
97 - printf(
98 - '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
99 - $message
100 - );
118 + printf(
119 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
120 + esc_html( $message )
121 + );
122 + }
101 123 }
102 124 }
103 125 }
104 126
@@ -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
@@ -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>
@@ -745,14 +840,14 @@
745 840 if (
746 841 get_option('propertyhive_maps_provider') !== 'osm' &&
747 842 get_option('propertyhive_maps_provider') !== 'mapbox' &&
748 843 get_option('propertyhive_google_maps_api_key', '') == '' &&
749 - !isset($_POST['propertyhive_google_maps_api_key']) &&
844 + ! $maps_api_key_submitted &&
750 845 (
751 - !isset($_GET['page'])
846 + ! $admin_page_present
752 847 ||
753 848 (
754 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
849 + $admin_page_present && 'ph-installed' !== $admin_page
755 850 )
756 851 ) &&
757 852 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
758 853 )
@@ -760,10 +855,10 @@
760 855 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
761 856 <p>
762 857 " . sprintf(
763 858 /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
764 - __( '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' ),
765 - admin_url('admin.php?page=ph-settings&tab=general&section=map')
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') )
766 861 ) . "
767 862 </p>
768 863 <p>
769 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>
@@ -776,12 +871,12 @@
776 871 if (
777 872 get_option('propertyhive_license_key', '') != '' &&
778 873 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
779 874 (
780 - !isset($_GET['page'])
875 + ! $admin_page_present
781 876 ||
782 877 (
783 - 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
784 879 )
785 880 )
786 881 )
787 882 {
@@ -815,8 +910,9 @@
815 910 $screen = get_current_screen();
816 911 if ( in_array( $screen->id, array( 'dashboard' ) ) )
817 912 {
818 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.
819 915 $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
820 916 $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
821 917 if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
822 918 {
@@ -832,9 +928,9 @@
832 928 }
833 929 }
834 930 }
835 931
836 - if ( isset($_GET['propertyhive_contacts_merged']) )
932 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
837 933 {
838 934 echo '
839 935 <div class="notice notice-info">
840 936 <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
@@ -853,8 +949,9 @@
853 949 {
854 950 delete_transient( '_ph_activation_redirect' );
855 951
856 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.
857 954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
858 955 return;
859 956 }
860 957
@@ -872,9 +969,10 @@
872 969
873 970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
874 971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
875 972 {
876 - exit( wp_redirect( home_url( '/' ) ) );
973 + wp_safe_redirect( home_url( '/' ) );
974 + exit;
877 975 }
878 976 }
879 977
880 978 /**
@@ -887,16 +985,26 @@
887 985 global $wpdb;
888 986
889 987 if ( isset( $_GET['view_propertyhive_email'] ) )
890 988 {
891 - 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' ) )
892 993 {
893 - die( 'Security check' );
994 + wp_die( 'Security check' );
894 995 }
895 996
997 + if ( ! current_user_can( 'manage_propertyhive' ) )
998 + {
999 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 + }
1001 +
896 1002 if ( isset( $_GET['email_id'] ) )
897 1003 {
898 - $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 ) );
899 1007 if ( null !== $email_log )
900 1008 {
901 1009 $body = $email_log->body;
902 1010
@@ -904,9 +1012,12 @@
904 1012 {
905 1013 $body = gzuncompress($body);
906 1014 }
907 1015
908 - 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;
909 1020
910 1021 }
911 1022 else
912 1023 {
@@ -925,23 +1036,29 @@
925 1036 */
926 1037 public function preview_emails() {
927 1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
928 1039 {
929 - 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' ) )
930 1044 {
931 1045 die( 'Security check' );
932 1046 }
933 1047
934 1048 $current_user = wp_get_current_user();
1049 + $request_get = wp_unslash( $_GET );
1050 + $request_post = wp_unslash( $_POST );
935 1051
936 1052 // get the preview email content
937 - if ( isset($_GET['property_id']) )
1053 + $email_property_ids = array();
1054 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
938 1055 {
939 - $email_property_ids = array((int)$_GET['property_id']);
1056 + $email_property_ids = array( absint( $request_get['property_id'] ) );
940 1057 }
941 - elseif ( isset($_POST['email_property_id']) )
1058 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
942 1059 {
943 - $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'] ) ) ) ) );
944 1061 }
945 1062
946 1063 $allowed_tags = array(
947 1064 'strong' => array(),
@@ -962,17 +1079,18 @@
962 1079 ),
963 1080 );
964 1081 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
965 1082
966 - $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 );
967 1085
968 - if ( isset($_GET['contact_id']) )
1086 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
969 1087 {
970 - $contact = new PH_Contact((int)$_GET['contact_id']);
971 - $body = str_replace("[contact_name]", $contact->post_title, $body);
972 - $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 );
973 1091 }
974 - $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 );
975 1093
976 1094 $office_counts = array();
977 1095
978 1096 if ( strpos($body, '[properties]') !== FALSE )
@@ -1013,22 +1131,23 @@
1013 1131 }
1014 1132
1015 1133 if ( !empty($office_id) )
1016 1134 {
1017 - $office_name = get_the_title($office_id);
1018 - $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 );
1019 1137 }
1020 1138
1021 - $body = str_replace("[office_name]", $office_name, $body);
1022 - $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 );
1023 1141
1024 - $body = str_replace("[negotiator_name]", $current_user->display_name, $body);
1025 - $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 );
1026 1144
1027 1145 // wrap the content with the email template and then add styles
1028 1146 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1029 1147
1030 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.
1031 1150 echo $message;
1032 1151 exit;
1033 1152 }
1034 1153 }