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 +247 -127 2.2.42.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
@@ -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
@@ -584,8 +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' );
674 + include_once( 'class-ph-admin-onboarding.php' );
588 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() ) {
@@ -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>
@@ -745,14 +841,14 @@
745 841 if (
746 842 get_option('propertyhive_maps_provider') !== 'osm' &&
747 843 get_option('propertyhive_maps_provider') !== 'mapbox' &&
748 844 get_option('propertyhive_google_maps_api_key', '') == '' &&
749 - !isset($_POST['propertyhive_google_maps_api_key']) &&
845 + ! $maps_api_key_submitted &&
750 846 (
751 - !isset($_GET['page'])
847 + ! $admin_page_present
752 848 ||
753 849 (
754 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
850 + $admin_page_present && 'ph-installed' !== $admin_page
755 851 )
756 852 ) &&
757 853 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
758 854 )
@@ -760,10 +856,10 @@
760 856 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
761 857 <p>
762 858 " . sprintf(
763 859 /* 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')
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') )
766 862 ) . "
767 863 </p>
768 864 <p>
769 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>
@@ -776,12 +872,12 @@
776 872 if (
777 873 get_option('propertyhive_license_key', '') != '' &&
778 874 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
779 875 (
780 - !isset($_GET['page'])
876 + ! $admin_page_present
781 877 ||
782 878 (
783 - 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
784 880 )
785 881 )
786 882 )
787 883 {
@@ -815,8 +911,9 @@
815 911 $screen = get_current_screen();
816 912 if ( in_array( $screen->id, array( 'dashboard' ) ) )
817 913 {
818 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.
819 916 $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
820 917 $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
821 918 if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
822 919 {
@@ -832,9 +929,9 @@
832 929 }
833 930 }
834 931 }
835 932
836 - if ( isset($_GET['propertyhive_contacts_merged']) )
933 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
837 934 {
838 935 echo '
839 936 <div class="notice notice-info">
840 937 <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
@@ -853,13 +950,14 @@
853 950 {
854 951 delete_transient( '_ph_activation_redirect' );
855 952
856 953 // Don't do redirect if part of multisite, doing batch-activate, or if no permission
857 - 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' ) ) {
858 956 return;
859 957 }
860 958
861 - wp_safe_redirect( admin_url( 'index.php?page=ph-installed' ) );
959 + wp_safe_redirect( admin_url( 'index.php?page=ph-onboarding' ) );
862 960 exit;
863 961 }
864 962 }
865 963
@@ -872,9 +970,10 @@
872 970
873 971 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
874 972 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
875 973 {
876 - exit( wp_redirect( home_url( '/' ) ) );
974 + wp_safe_redirect( home_url( '/' ) );
975 + exit;
877 976 }
878 977 }
879 978
880 979 /**
@@ -887,16 +986,26 @@
887 986 global $wpdb;
888 987
889 988 if ( isset( $_GET['view_propertyhive_email'] ) )
890 989 {
891 - 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' ) )
892 994 {
893 - die( 'Security check' );
995 + wp_die( 'Security check' );
894 996 }
895 997
998 + if ( ! current_user_can( 'manage_propertyhive' ) )
999 + {
1000 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1001 + }
1002 +
896 1003 if ( isset( $_GET['email_id'] ) )
897 1004 {
898 - $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 ) );
899 1008 if ( null !== $email_log )
900 1009 {
901 1010 $body = $email_log->body;
902 1011
@@ -904,9 +1013,12 @@
904 1013 {
905 1014 $body = gzuncompress($body);
906 1015 }
907 1016
908 - 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;
909 1021
910 1022 }
911 1023 else
912 1024 {
@@ -925,23 +1037,29 @@
925 1037 */
926 1038 public function preview_emails() {
927 1039 if ( isset( $_GET['preview_propertyhive_email'] ) )
928 1040 {
929 - 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' ) )
930 1045 {
931 1046 die( 'Security check' );
932 1047 }
933 1048
934 1049 $current_user = wp_get_current_user();
1050 + $request_get = wp_unslash( $_GET );
1051 + $request_post = wp_unslash( $_POST );
935 1052
936 1053 // get the preview email content
937 - if ( isset($_GET['property_id']) )
1054 + $email_property_ids = array();
1055 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
938 1056 {
939 - $email_property_ids = array((int)$_GET['property_id']);
1057 + $email_property_ids = array( absint( $request_get['property_id'] ) );
940 1058 }
941 - elseif ( isset($_POST['email_property_id']) )
1059 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
942 1060 {
943 - $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'] ) ) ) ) );
944 1062 }
945 1063
946 1064 $allowed_tags = array(
947 1065 'strong' => array(),
@@ -962,17 +1080,18 @@
962 1080 ),
963 1081 );
964 1082 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
965 1083
966 - $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 );
967 1086
968 - if ( isset($_GET['contact_id']) )
1087 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
969 1088 {
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);
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 );
973 1092 }
974 - $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 );
975 1094
976 1095 $office_counts = array();
977 1096
978 1097 if ( strpos($body, '[properties]') !== FALSE )
@@ -1013,22 +1132,23 @@
1013 1132 }
1014 1133
1015 1134 if ( !empty($office_id) )
1016 1135 {
1017 - $office_name = get_the_title($office_id);
1018 - $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 );
1019 1138 }
1020 1139
1021 - $body = str_replace("[office_name]", $office_name, $body);
1022 - $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 );
1023 1142
1024 - $body = str_replace("[negotiator_name]", $current_user->display_name, $body);
1025 - $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 );
1026 1145
1027 1146 // wrap the content with the email template and then add styles
1028 1147 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1029 1148
1030 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.
1031 1151 echo $message;
1032 1152 exit;
1033 1153 }
1034 1154 }