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 +748 -70 1.4.492.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
@@ -23,8 +27,9 @@
23 27 add_action( 'current_screen', array( $this, 'conditional_includes' ) );
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') );
31 + add_action( 'admin_notices', array( $this, 'archive_admin_notices' ) );
27 32 add_action( 'admin_menu', array( $this, 'admin_dashboard_pages' ) );
28 33 add_action( 'admin_head', array( $this, 'admin_head' ) );
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
@@ -29,15 +34,515 @@
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
31 36 add_action( 'admin_init', array( $this, 'view_email' ) );
32 37 add_action( 'admin_init', array( $this, 'preview_emails' ) );
38 + add_action( 'admin_init', array( $this, 'record_recently_viewed' ) );
39 + add_action( 'admin_init', array( $this, 'export_applicant_list' ) );
40 + add_action( 'admin_init', array( $this, 'export_sub_grid' ) );
41 + add_action( 'admin_init', array( $this, 'check_hide_demo_data_tab' ) );
42 + add_action( 'admin_init', array( $this, 'check_install_add_on' ) );
43 + add_filter( 'propertyhive_screen_ids', array( $this, 'crm_only_mode_screen_id' ) );
33 44 }
45 +
46 + public function archive_admin_notices()
47 + {
48 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
49 + if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts']))
50 + {
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']) ) : '';
53 + if ( $post_type )
54 + {
55 + $post_type_object = get_post_type_object($post_type);
56 + if ( ! $post_type_object ) {
57 + return;
58 + }
59 +
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;
62 +
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 + }
84 + }
85 + }
86 +
87 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
88 + if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) )
89 + {
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']) ) : '';
92 + if ( $post_type )
93 + {
94 + $post_type_object = get_post_type_object($post_type);
95 + if ( ! $post_type_object ) {
96 + return;
97 + }
98 +
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;
101 +
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 + }
123 + }
124 + }
125 + }
126 +
127 + public function crm_only_mode_screen_id( $screen_ids )
128 + {
129 + $current_user = wp_get_current_user();
130 +
131 + $user_id = $current_user->ID;
132 +
133 + $crm_only_mode = get_user_meta( $user_id, 'crm_only_mode', TRUE );
134 +
135 + if ( $crm_only_mode == '1' )
136 + {
137 + $screen_ids[] = 'toplevel_page_ph-settings';
138 + }
139 +
140 + return $screen_ids;
141 + }
142 +
143 + public function check_install_add_on()
144 + {
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 )
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 +
157 + $installed_plugins = get_option( 'propertyhive_pre_pro_add_ons', array());
158 +
159 + if ( empty($installed_plugins) )
160 + {
161 + $installed_plugins = array();
162 + }
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 +
170 + $installed_plugins[] = array(
171 + 'slug' => ph_clean( $decoded_slug ),
172 + 'plugin' => ph_clean( $decoded_plugin )
173 + );
174 +
175 + update_option( 'propertyhive_pre_pro_add_ons', $installed_plugins );
176 +
177 + wp_safe_redirect( admin_url('admin.php?page=ph-settings&tab=features') );
178 + die();
179 + }
180 + }
181 +
182 + public function check_hide_demo_data_tab()
183 + {
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 )
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 +
195 + update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
196 + wp_safe_redirect( admin_url('admin.php?page=ph-settings') );
197 + die();
198 + }
199 + }
200 +
201 + public function export_sub_grid()
202 + {
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 )
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 +
235 + ob_start();
236 +
237 + $df = fopen("php://output", 'w');
238 +
239 + $columns = array( 'id' => __( 'ID', 'propertyhive' ) );
240 +
241 + if ( strpos( $sub_grid, 'viewings' ) !== false )
242 + {
243 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
244 + $columns['property'] = __( 'Property', 'propertyhive' );
245 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
246 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
247 + $columns['negotiator'] = __( 'Attending Negotiator(s)', 'propertyhive' );
248 + $columns['status'] = __( 'Status', 'propertyhive' );
249 + $columns['feedback'] = __( 'Feedback', 'propertyhive' );
250 + }
251 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
252 + {
253 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
254 + $columns['property'] = __( 'Property', 'propertyhive' );
255 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
256 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
257 + $columns['status'] = __( 'Status', 'propertyhive' );
258 + $columns['amount'] = __( 'Offer Amount', 'propertyhive' );
259 + }
260 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
261 + {
262 + $columns['date'] = __( 'Date', 'propertyhive' );
263 + $columns['property'] = __( 'Property', 'propertyhive' );
264 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
265 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
266 + $columns['status'] = __( 'Status', 'propertyhive' );
267 + $columns['amount'] = __( 'Sale Amount', 'propertyhive' );
268 + }
269 +
270 + fputcsv($df, $columns);
271 +
272 + if ( ! empty( $record_ids ) )
273 + {
274 + if ( !empty($record_ids) )
275 + {
276 + if ( strpos( $sub_grid, 'viewings' ) !== false )
277 + {
278 + $args = array(
279 + 'post_type' => 'viewing',
280 + 'nopaging' => TRUE,
281 + 'fields' => 'ids',
282 + 'post__in' => $record_ids,
283 + 'order' => 'ASC',
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.
286 + 'meta_key' => '_start_date_time',
287 + );
288 +
289 + $records_query = new WP_Query( $args );
290 +
291 + if ( $records_query->have_posts() )
292 + {
293 + while ( $records_query->have_posts() )
294 + {
295 + $records_query->the_post();
296 +
297 + $viewing = new PH_Viewing( get_the_ID() );
298 +
299 + $property_id = (int)$viewing->_property_id;
300 + $property_address = '';
301 + if ( !empty($property_id) )
302 + {
303 + $property = new PH_Property( $property_id );
304 + $property_address = $property->get_formatted_full_address();
305 + }
306 +
307 + $columns = array(
308 + get_the_ID(),
309 + gmdate("H:i jS F Y", strtotime($viewing->_start_date_time)),
310 + $property_address,
311 + str_replace("<br>", "\n", $viewing->get_applicants()),
312 + $viewing->get_negotiators(),
313 + str_replace("<br>", "\n", $viewing->get_status()),
314 + $viewing->_feedback
315 + );
316 +
317 + fputcsv($df, $columns);
318 + }
319 + }
320 + }
321 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
322 + {
323 + $args = array(
324 + 'post_type' => 'offer',
325 + 'nopaging' => TRUE,
326 + 'fields' => 'ids',
327 + 'post__in' => $record_ids,
328 + 'order' => 'ASC',
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.
331 + 'meta_key' => '_offer_date_time',
332 + );
333 +
334 + $records_query = new WP_Query( $args );
335 +
336 + if ( $records_query->have_posts() )
337 + {
338 + while ( $records_query->have_posts() )
339 + {
340 + $records_query->the_post();
341 +
342 + $offer = new PH_Offer( get_the_ID() );
343 +
344 + $property_id = (int)$offer->_property_id;
345 + $property_address = '';
346 + if ( !empty($property_id) )
347 + {
348 + $property = new PH_Property( $property_id );
349 + $property_address = $property->get_formatted_full_address();
350 + }
351 +
352 + $columns = array(
353 + get_the_ID(),
354 + gmdate("H:i jS F Y", strtotime($offer->_offer_date_time)),
355 + $property_address,
356 + str_replace("<br>", "\n", $offer->get_applicants()),
357 + $offer->_status,
358 + html_entity_decode($offer->get_formatted_amount())
359 + );
360 +
361 + fputcsv($df, $columns);
362 + }
363 + }
364 + }
365 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
366 + {
367 + $args = array(
368 + 'post_type' => 'sale',
369 + 'nopaging' => TRUE,
370 + 'fields' => 'ids',
371 + 'post__in' => $record_ids,
372 + 'order' => 'ASC',
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.
375 + 'meta_key' => '_sale_date_time',
376 + );
377 +
378 + $records_query = new WP_Query( $args );
379 +
380 + if ( $records_query->have_posts() )
381 + {
382 + while ( $records_query->have_posts() )
383 + {
384 + $records_query->the_post();
385 +
386 + $sale = new PH_Sale( get_the_ID() );
387 +
388 + $property_id = (int)$sale->_property_id;
389 + $property_address = '';
390 + if ( !empty($property_id) )
391 + {
392 + $property = new PH_Property( $property_id );
393 + $property_address = $property->get_formatted_full_address();
394 + }
395 +
396 + $columns = array(
397 + get_the_ID(),
398 + gmdate("jS F Y", strtotime($sale->_sale_date_time)),
399 + $property_address,
400 + str_replace("<br>", "\n", $sale->get_applicants()),
401 + $sale->_status,
402 + html_entity_decode($sale->get_formatted_amount())
403 + );
404 +
405 + fputcsv($df, $columns);
406 + }
407 + }
408 + }
409 + }
410 + }
411 +
412 + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
413 +
414 + $output = ob_get_clean();
415 +
416 + $filename = sanitize_title( $sub_grid ) . '-' . gmdate("YmdHis") . '.csv';
417 +
418 + // disable caching
419 + $now = gmdate("D, d M Y H:i:s");
420 + header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
421 + header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
422 + header("Last-Modified: {$now} GMT");
423 +
424 + // force download
425 + header("Content-Type: application/force-download");
426 + header("Content-Type: application/octet-stream");
427 + header("Content-Type: application/download");
428 +
429 + // disposition / encoding on response body
430 + header("Content-Disposition: attachment;filename={$filename}");
431 + header("Content-Transfer-Encoding: binary");
432 +
433 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download produced by fputcsv, not HTML; HTML escaping would corrupt exported field values.
434 + echo $output;
435 +
436 + die();
437 + }
438 + }
439 +
440 + public function export_applicant_list()
441 + {
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 )
450 + {
451 + include_once( 'class-ph-admin-applicant-list.php' );
452 + $ph_admin_applicant_list = new PH_Admin_Applicant_List();
453 + $ph_admin_applicant_list->export();
454 + }
455 + }
456 +
457 + public function record_recently_viewed()
458 + {
459 + global $pagenow;
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 +
466 + if (
467 + 'post.php' === $pagenow &&
468 + $recent_post_id > 0 &&
469 + in_array(
470 + get_post_type( $recent_post_id ),
471 + apply_filters( 'propertyhive_post_types_with_tabs', array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale') )
472 + )
473 + )
474 + {
475 + $recently_viewed = get_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', TRUE );
476 +
477 + if ( !is_array($recently_viewed) )
478 + {
479 + $recently_viewed = array();
480 + }
481 +
482 + foreach ( $recently_viewed as $time => $post )
483 + {
484 + if ( $recent_post_id == $post['id'] )
485 + {
486 + unset($recently_viewed[$time]);
487 + }
488 + }
489 +
490 + $title = get_the_title( $recent_post_id );
491 +
492 + switch ( get_post_type( $recent_post_id ) )
493 + {
494 + case "appraisal":
495 + {
496 + $appraisal = new PH_Appraisal( $recent_post_id );
497 + $title = $appraisal->get_formatted_summary_address();
498 + break;
499 + }
500 + case "property":
501 + {
502 + $property = new PH_Property( $recent_post_id );
503 + $title = $property->get_formatted_summary_address();
504 + break;
505 + }
506 + case "enquiry":
507 + case "viewing":
508 + case "offer":
509 + case "sale":
510 + {
511 + $property_id = get_post_meta( $recent_post_id, '_property_id', TRUE );
512 + if ( $property_id != '' )
513 + {
514 + $property = new PH_Property( (int)$property_id );
515 + $title = $property->get_formatted_summary_address();
516 + }
517 + break;
518 + }
519 + }
520 +
521 + $title = ucfirst( get_post_type( $recent_post_id ) ) . ' - ' . $title;
522 +
523 + $recently_viewed = array(time() => array(
524 + 'id' => $recent_post_id,
525 + 'title' => $title,
526 + 'post_type' => get_post_type( $recent_post_id ),
527 + 'edit_link' => get_edit_post_link( $recent_post_id ),
528 + )) + $recently_viewed;
529 +
530 + $recently_viewed = array_slice($recently_viewed, 0, 10, TRUE);
531 +
532 + update_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', $recently_viewed );
533 + }
534 + }
34 535
35 536 public function admin_dashboard_pages()
36 537 {
37 - 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 )
38 543 {
39 - switch ( sanitize_title($_GET['page']) )
544 + switch ( $admin_page )
40 545 {
41 546 case 'ph-installed':
42 547 {
43 548 add_dashboard_page(
@@ -43,9 +548,9 @@
43 548 add_dashboard_page(
44 549 __( 'Welcome to Property Hive', 'propertyhive' ),
45 550 __( 'Welcome to Property Hive', 'propertyhive' ),
46 551 'manage_propertyhive',
47 - sanitize_title($_GET['page']),
552 + $admin_page,
48 553 array( $this, 'installed_screen' )
49 554 );
50 555
51 556 break;
@@ -58,9 +563,9 @@
58 563 {
59 564 ?>
60 565 <div class="wrap propertyhive-installed-screen">
61 566
62 - <h1><?php _e( 'Welcome to Property Hive', 'propertyhive' ); ?></h1>
567 + <h1><?php echo esc_html(__( 'Welcome to Property Hive', 'propertyhive' )); ?></h1>
63 568
64 569 <div class="intro-text">
65 570 <p>Thank you choosing Property Hive to power your next property website. Below you'll find useful links, tips on getting started, and more.</p>
66 571 </div>
@@ -72,13 +577,13 @@
72 577 <h2>Getting Started</h2>
73 578
74 579 <p>Now that you've installed Property Hive you'll notice a new 'Property Hive' item in the left hand menu of WordPress.</p>
75 580
76 - <img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
581 + <img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
77 582
78 - <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo admin_url( 'admin.php?page=ph-settings' ); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
583 + <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo esc_url(admin_url( 'admin.php?page=ph-settings' )); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
79 584
80 - <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo admin_url( 'post-new.php?post_type=property' ); ?>" target="_blank">adding your first property</a>.</p>
585 + <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo esc_url(admin_url( 'post-new.php?post_type=property' )); ?>" target="_blank">adding your first property</a>.</p>
81 586
82 587 </div>
83 588
84 589 <div class="panel">
@@ -86,9 +591,9 @@
86 591 <h2>Extending Property Hive</h2>
87 592
88 593 <p>We have a <a href="https://wp-property-hive.com/add-ons/" target="_blank">wide range of add ons</a> available to add extra functionality to your website.</p>
89 594
90 - <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
595 + <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
91 596
92 597 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=free" target="_blank">Free Add Ons</a></strong><br>
93 598 From our template assistant add on to a variety of calculators, these free add ons are great additions to any property website.</p>
94 599
@@ -97,10 +602,10 @@
97 602
98 603 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=tools" target="_blank">Internal Tools</a></strong><br>
99 604 Add ons aimed to make your life easier and to save you time. Includes Digital Window Displays, Address Lookup and more.</p>
100 605
101 - <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import-export" target="_blank">Import and Export</a></strong><br>
102 - Send your properties to portals like Rightmove, Zoopla and more or import properties from thid party software. These add ons automate the import and export of property data.</p>
606 + <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import" target="_blank">Import and Export</a></strong><br>
607 + Import properties from third party software or send your properties to portals like Rightmove, Zoopla and more. These add ons automate the import and export of property data.</p>
103 608
104 609 </div>
105 610
106 611 <div class="panel">
@@ -109,13 +614,10 @@
109 614
110 615 We pride ourselves on great support at Property Hive and will always do what we can to help you make create the best site possible. Please find below some useful links relating to our support:
111 616
112 617 <p><strong style="font-size:14px;">Documentation</strong><br>
113 - We have documentation <a href="https://wp-property-hive.com/documentation/" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
618 + We have documentation <a href="https://docs.wp-property-hive.com" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
114 619
115 - <p><strong style="font-size:14px;">Priority One-To-One Support</strong><br>
116 - If you require help quickly, or wish to discuss a bespoke requirement, then <a href="https://wp-property-hive.com/product/12-month-license-key/" target="_blank">priority support</a> might be best for you. With a license key priced at just £49.99 per year you'll not only get priority support but also updates to any add ons you've purchased.</p>
117 -
118 620 <p><strong style="font-size:14px;">Our Support Policy</strong><br>
119 621 Our <a href="https://wp-property-hive.com/support-policy/" target="_blank">Support Policy is available to view here</a> and outlines how you can get in touch, how we will (and won't) help, and how to report bugs.</p>
120 622
121 623 </div>
@@ -126,12 +628,12 @@
126 628
127 629 <p><strong style="font-size:14px;">Need a Theme?</strong><br>
128 630 Property Hive does <a href="https://wp-property-hive.com/which-wordpress-themes-work-with-property-hive/" target="_blank">integrate with any new or existing theme</a>. If however you need to get up and running quickly, or just want to have a play before committing, then our free <a href="https://wp-property-hive.com/honeycomb" target="_blank">Honeycomb theme</a> might be right for you.</p>
129 631
130 - <a href="https://wp-property-hive.com/honeycomb" target="_blank"><img src="<?php echo 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>
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>
131 633
132 634 <p><strong style="font-size:14px;">Leave a Review</strong><br>
133 - 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>
134 636
135 637 <p><strong style="font-size:14px;">Contribute</strong><br>
136 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>
137 639
@@ -168,14 +670,20 @@
168 670 include_once( 'ph-meta-box-functions.php' );
169 671
170 672 // Classes
171 673 include_once( 'class-ph-admin-post-types.php' );
172 - //include_once( 'class-ph-admin-taxonomies.php' );
674 + include_once( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
173 675
174 676 // Classes we only need if the ajax is not-ajax
175 677 if ( ! is_ajax() ) {
176 678 include( 'class-ph-admin-menus.php' );
177 679 include( 'class-ph-admin-assets.php' );
680 +
681 + // Help Tab
682 + if ( apply_filters( 'propertyhive_enable_admin_help_tab', true ) )
683 + {
684 + include_once( 'class-ph-admin-help.php' );
685 + }
178 686 }
179 687 }
180 688
181 689 /**
@@ -192,8 +700,14 @@
192 700 break;
193 701 case 'plugins' :
194 702 include( 'class-ph-admin-plugin-updates.php' );
195 703 break;
704 + case 'users':
705 + case 'user':
706 + case 'profile':
707 + case 'user-edit':
708 + include( 'class-ph-admin-profile.php' );
709 + break;
196 710 }
197 711 }
198 712
199 713 /**
@@ -225,8 +739,19 @@
225 739 }
226 740
227 741 public function review_admin_notices()
228 742 {
743 + global $wpdb;
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 +
229 754 if ( current_user_can( 'manage_options' ) )
230 755 {
231 756 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
232 757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -234,12 +759,12 @@
234 759 if ( $propertyhive_review_prompt_due_timestamp < time() )
235 760 {
236 761 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
237 762 <p>
238 - " . __( '<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' ) ) . "
239 764 </p>
240 765 <p>
241 - <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>
242 767 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
243 768 </p>
244 769 </div>";
245 770 }
@@ -245,14 +770,57 @@
245 770 }
246 771 }
247 772
248 773 if (
774 + class_exists('Easy_Property_Listings') &&
775 + ! $plugin_status_present &&
776 + get_option( 'epl_notice_dismissed', '' ) != 'yes'
777 + )
778 + {
779 + echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\">
780 + <p>
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' ) ) . "
782 + </p>
783 + <p>
784 + <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a>
785 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a>
786 + </p>
787 +
788 + </div>";
789 + }
790 +
791 + if (
792 + !class_exists('PH_Demo_Data') &&
793 + get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 &&
794 + get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' &&
795 + (
796 + ! $admin_page_present
797 + ||
798 + (
799 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
800 + )
801 + )
802 + )
803 + {
804 + echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\">
805 + <p>
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' ) ) . "
807 + </p>
808 + <p>
809 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a>
810 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a>
811 + </p>
812 +
813 + </div>";
814 + }
815 +
816 + if (
249 817 get_option('propertyhive_search_results_page_id', '') == '' &&
250 818 (
251 - !isset($_GET['page'])
819 + ! $admin_page_present
252 820 ||
253 821 (
254 - 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
255 823 )
256 824 ) &&
257 825 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
258 826 )
@@ -258,13 +826,13 @@
258 826 )
259 827 {
260 828 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
261 829 <p>
262 - " . __( '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' ) . "
263 831 </p>
264 832 <p>
265 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general') . "\" class=\"button-primary\">Go To Property Hive Settings</a>
266 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">Dismiss</a>
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>
834 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
267 835 </p>
268 836
269 837 </div>";
270 838 }
@@ -269,15 +837,17 @@
269 837 </div>";
270 838 }
271 839
272 840 if (
841 + get_option('propertyhive_maps_provider') !== 'osm' &&
842 + get_option('propertyhive_maps_provider') !== 'mapbox' &&
273 843 get_option('propertyhive_google_maps_api_key', '') == '' &&
274 - !isset($_POST['propertyhive_google_maps_api_key']) &&
844 + ! $maps_api_key_submitted &&
275 845 (
276 - !isset($_GET['page'])
846 + ! $admin_page_present
277 847 ||
278 848 (
279 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
849 + $admin_page_present && 'ph-installed' !== $admin_page
280 850 )
281 851 ) &&
282 852 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
283 853 )
@@ -283,13 +853,17 @@
283 853 )
284 854 {
285 855 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
286 856 <p>
287 - " . __( '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="'. admin_url('admin.php?page=ph-settings&tab=general&section=map') . '">enter it</a>.', 'propertyhive' ) . "
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 + ) . "
288 862 </p>
289 863 <p>
290 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general&section=map') . "\" class=\"button-primary\">Enter Google Maps API Key</a>
291 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">Dismiss</a>
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>
865 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
292 866 </p>
293 867
294 868 </div>";
295 869 }
@@ -297,12 +871,12 @@
297 871 if (
298 872 get_option('propertyhive_license_key', '') != '' &&
299 873 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
300 874 (
301 - !isset($_GET['page'])
875 + ! $admin_page_present
302 876 ||
303 877 (
304 - 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
305 879 )
306 880 )
307 881 )
308 882 {
@@ -310,32 +884,13 @@
310 884 $output = '';
311 885
312 886 if ( isset($license['active']) && $license['active'] != '1' )
313 887 {
314 - $output = __( 'You\'re Property Hive license key is inactive.', 'propertyhive' );
888 + $output = __( 'Your Property Hive license key is inactive.', 'propertyhive' );
315 889 }
316 890 else
317 891 {
318 - if ( isset($license['expires_at']) && $license['expires_at'] != '' )
319 - {
320 - if ( strtotime($license['expires_at']) <= time() )
321 - {
322 - // Expired
323 - $output = __( 'Your Property Hive license key expired on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
324 - }
325 - elseif (
326 - strtotime($license['expires_at']) > time() &&
327 - strtotime($license['expires_at']) < (time() + 30 * 24 * 60 * 60)
328 - )
329 - {
330 - // Expires in less than 30 days
331 - $output = __( 'Your Property Hive license key expires on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
332 - }
333 - elseif (strtotime($license['expires_at']) > time())
334 - {
335 - // Valid
336 - }
337 - }
892 +
338 893 }
339 894
340 895 if ( $output != '' )
341 896 {
@@ -340,19 +895,49 @@
340 895 if ( $output != '' )
341 896 {
342 897 echo "<div class=\"notice notice-info\" id=\"ph_notice_invalid_expired_license_key\">
343 898 <p>
344 - " . $output . "
899 + " . esc_html($output) . "
345 900 </p>
346 901 <p>
347 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=licensekey') . "\" class=\"button-primary\">Go To License Key Settings</a>
348 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">Dismiss</a>
902 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=licensekey')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To License Key Settings', 'propertyhive' )) . "</a>
903 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
349 904 </p>
350 905
351 906 </div>";
352 907 }
353 908 }
909 +
910 + $screen = get_current_screen();
911 + if ( in_array( $screen->id, array( 'dashboard' ) ) )
912 + {
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.
915 + $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
916 + $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
917 + if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
918 + {
919 + echo '
920 + <div class="notice notice-error" id="ph_notice_email_cron_not_running">
921 + <p>' . esc_html(__( 'The Property Hive email queue does not appear to be running', 'propertyhive' )) . '
922 + </p>
923 + <p>
924 + <a href="'. esc_url(admin_url('admin.php?page=ph-settings&tab=email&section=log&status=queued')) . '" class="button-primary">' . esc_html(__( 'Go To Email Queue', 'propertyhive' )) . '</a>
925 + </p>
926 + </div>
927 + ';
928 + }
929 + }
354 930 }
931 +
932 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
933 + {
934 + echo '
935 + <div class="notice notice-info">
936 + <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
937 + </div>
938 + ';
939 + }
355 940 }
356 941
357 942 /**
358 943 * Handle redirects to welcome page after install.
@@ -364,8 +949,9 @@
364 949 {
365 950 delete_transient( '_ph_activation_redirect' );
366 951
367 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.
368 954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
369 955 return;
370 956 }
371 957
@@ -383,9 +969,10 @@
383 969
384 970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
385 971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
386 972 {
387 - exit( wp_redirect( home_url( '/' ) ) );
973 + wp_safe_redirect( home_url( '/' ) );
974 + exit;
388 975 }
389 976 }
390 977
391 978 /**
@@ -398,19 +985,39 @@
398 985 global $wpdb;
399 986
400 987 if ( isset( $_GET['view_propertyhive_email'] ) )
401 988 {
402 - 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' ) )
403 993 {
404 - die( 'Security check' );
994 + wp_die( 'Security check' );
405 995 }
406 996
997 + if ( ! current_user_can( 'manage_propertyhive' ) )
998 + {
999 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 + }
1001 +
407 1002 if ( isset( $_GET['email_id'] ) )
408 1003 {
409 - $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 ) );
410 1007 if ( null !== $email_log )
411 1008 {
412 - echo apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $email_log->body ) ) );
1009 + $body = $email_log->body;
1010 +
1011 + if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
1012 + {
1013 + $body = gzuncompress($body);
1014 + }
1015 +
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;
413 1020
414 1021 }
415 1022 else
416 1023 {
@@ -429,28 +1036,64 @@
429 1036 */
430 1037 public function preview_emails() {
431 1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
432 1039 {
433 - 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' ) )
434 1044 {
435 1045 die( 'Security check' );
436 1046 }
437 1047
1048 + $current_user = wp_get_current_user();
1049 + $request_get = wp_unslash( $_GET );
1050 + $request_post = wp_unslash( $_POST );
1051 +
438 1052 // get the preview email content
439 - if ( isset($_GET['property_id']) )
1053 + $email_property_ids = array();
1054 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
440 1055 {
441 - $email_property_ids = array((int)$_GET['property_id']);
1056 + $email_property_ids = array( absint( $request_get['property_id'] ) );
442 1057 }
443 - elseif ( isset($_POST['email_property_id']) )
1058 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
444 1059 {
445 - $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'] ) ) ) ) );
446 1061 }
447 1062
448 - $body = stripslashes(sanitize_textarea_field($_POST['body']));
1063 + $allowed_tags = array(
1064 + 'strong' => array(),
1065 + 'span' => array(),
1066 + 'em' => array(),
1067 + 'h1' => array(),
1068 + 'h2' => array(),
1069 + 'h3' => array(),
1070 + 'h4' => array(),
1071 + 'h5' => array(),
1072 + 'h6' => array(),
1073 + 'i' => array(),
1074 + 'u' => array(),
1075 + 'b' => array(),
1076 + 'a' => array(
1077 + 'href' => array(),
1078 + 'target' => array(),
1079 + ),
1080 + );
1081 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
449 1082
450 - $body = str_replace("[contact_name]", get_the_title((int)$_GET['contact_id']), $body);
451 - $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
1083 + $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1084 + $body = wp_kses( $raw_body, $allowed_tags );
452 1085
1086 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
1087 + {
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 );
1091 + }
1092 + $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body );
1093 +
1094 + $office_counts = array();
1095 +
453 1096 if ( strpos($body, '[properties]') !== FALSE )
454 1097 {
455 1098 ob_start();
456 1099
@@ -458,8 +1101,15 @@
458 1101 {
459 1102 foreach ( $email_property_ids as $email_property_id )
460 1103 {
461 1104 $property = new PH_Property((int)$email_property_id);
1105 +
1106 + if ( $property->office_id != '' && $property->office_id != 0 )
1107 + {
1108 + if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
1109 + ++$office_counts[$property->office_id];
1110 + }
1111 +
462 1112 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
463 1113 }
464 1114 }
465 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -464,12 +1114,40 @@
464 1114 }
465 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
466 1116 }
467 1117
1118 + $office_name = '';
1119 + $office_email_address = '';
1120 +
1121 + $office_id = get_user_meta($current_user->ID, 'office_id', TRUE);
1122 + if ($office_id == '')
1123 + {
1124 + // No office against user. Use email address of office with most properties
1125 + if ( !empty($office_counts) )
1126 + {
1127 + arsort($office_counts);
1128 + reset($office_counts);
1129 + $office_id = key($office_counts);
1130 + }
1131 + }
1132 +
1133 + if ( !empty($office_id) )
1134 + {
1135 + $office_name = get_the_title( (int) $office_id );
1136 + $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE );
1137 + }
1138 +
1139 + $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
1140 + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
1141 +
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 );
1144 +
468 1145 // wrap the content with the email template and then add styles
469 1146 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
470 1147
471 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.
472 1150 echo $message;
473 1151 exit;
474 1152 }
475 1153 }
@@ -474,5 +1152,5 @@
474 1152 }
475 1153 }
476 1154 }
477 1155
478 -return new PH_Admin();
1156 +return new PH_Admin();