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
propertyhive / includes / admin / class-ph-admin.php

class-ph-admin.php in Property Hive 2.4.0, at includes/admin/class-ph-admin.php

1,158 lines 53.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit; // Exit if accessed directly
7 }
8
9 /**
10 * PropertyHive Admin.
11 *
12 * @class PH_Admin
13 * @author PropertyHive
14 * @category Admin
15 * @package PropertyHive/Admin
16 * @version 1.0.0
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.
19 class PH_Admin {
20
21 /**
22 * Constructor
23 */
24 public function __construct()
25 {
26 add_action( 'init', array( $this, 'includes' ) );
27 add_action( 'current_screen', array( $this, 'conditional_includes' ) );
28 add_action( 'current_screen', array( $this, 'disable_propertyhive_meta_box_dragging' ) );
29 add_action( 'current_screen', array( $this, 'remove_propertyhive_meta_boxes_from_screen_options' ) );
30 add_action( 'admin_notices', array( $this, 'review_admin_notices') );
31 add_action( 'admin_notices', array( $this, 'archive_admin_notices' ) );
32 add_action( 'admin_menu', array( $this, 'admin_dashboard_pages' ) );
33 add_action( 'admin_head', array( $this, 'admin_head' ) );
34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
36 add_action( 'admin_init', array( $this, 'view_email' ) );
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' ) );
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 }
535
536 public function admin_dashboard_pages()
537 {
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 )
543 {
544 switch ( $admin_page )
545 {
546 case 'ph-installed':
547 {
548 add_dashboard_page(
549 __( 'Welcome to Property Hive', 'propertyhive' ),
550 __( 'Welcome to Property Hive', 'propertyhive' ),
551 'manage_propertyhive',
552 $admin_page,
553 array( $this, 'installed_screen' )
554 );
555
556 break;
557 }
558 }
559 }
560 }
561
562 public function installed_screen()
563 {
564 ?>
565 <div class="wrap propertyhive-installed-screen">
566
567 <h1><?php echo esc_html(__( 'Welcome to Property Hive', 'propertyhive' )); ?></h1>
568
569 <div class="intro-text">
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>
571 </div>
572
573 <div class="panels">
574
575 <div class="panel">
576
577 <h2>Getting Started</h2>
578
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>
580
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">
582
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>
584
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>
586
587 </div>
588
589 <div class="panel">
590
591 <h2>Extending Property Hive</h2>
592
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>
594
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>
596
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>
598 From our template assistant add on to a variety of calculators, these free add ons are great additions to any property website.</p>
599
600 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=enhancements" target="_blank">Website Enhancements</a></strong><br>
601 Map View, Radial Search, Property Shortlist, Infinite Scroll, and lots more. Wow your users with the functionality provided with these add ons.</p>
602
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>
604 Add ons aimed to make your life easier and to save you time. Includes Digital Window Displays, Address Lookup and more.</p>
605
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>
608
609 </div>
610
611 <div class="panel">
612
613 <h2>Support</h2>
614
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:
616
617 <p><strong style="font-size:14px;">Documentation</strong><br>
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>
619
620 <p><strong style="font-size:14px;">Our Support Policy</strong><br>
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>
622
623 </div>
624
625 <div class="panel">
626
627 <h2>Additional Information</h2>
628
629 <p><strong style="font-size:14px;">Need a Theme?</strong><br>
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>
631
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>
633
634 <p><strong style="font-size:14px;">Leave a Review</strong><br>
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>
636
637 <p><strong style="font-size:14px;">Contribute</strong><br>
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>
639
640 <p><strong style="font-size:14px;">Our Feature Roadmap</strong><br>
641 View our <a href="https://trello.com/b/jb7bjB6j/property-hive-roadmap" target="_blank">feature roadmap</a> to see what's coming up, vote on feature, or submit your own ideas.</p>
642
643
644 </div>
645
646 </div>
647
648 </div>
649 <?php
650 }
651
652 /**
653 * Hide Individual Dashboard Pages
654 *
655 * @access public
656 * @since 1.0
657 * @return void
658 */
659 public function admin_head()
660 {
661 remove_submenu_page( 'index.php', 'ph-installed' );
662 }
663
664 /**
665 * Include any classes we need within admin.
666 */
667 public function includes() {
668 // Functions
669 include_once( 'ph-admin-functions.php' );
670 include_once( 'ph-meta-box-functions.php' );
671
672 // Classes
673 include_once( 'class-ph-admin-post-types.php' );
674 include_once( 'class-ph-admin-onboarding.php' );
675 include_once( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
676
677 // Classes we only need if the ajax is not-ajax
678 if ( ! is_ajax() ) {
679 include( 'class-ph-admin-menus.php' );
680 include( 'class-ph-admin-assets.php' );
681
682 // Help Tab
683 if ( apply_filters( 'propertyhive_enable_admin_help_tab', true ) )
684 {
685 include_once( 'class-ph-admin-help.php' );
686 }
687 }
688 }
689
690 /**
691 * Include admin files conditionally.
692 */
693 public function conditional_includes() {
694 if ( ! $screen = get_current_screen() ) {
695 return;
696 }
697
698 switch ( $screen->id ) {
699 case 'dashboard' :
700 include( 'class-ph-admin-dashboard.php' );
701 break;
702 case 'plugins' :
703 include( 'class-ph-admin-plugin-updates.php' );
704 break;
705 case 'users':
706 case 'user':
707 case 'profile':
708 case 'user-edit':
709 include( 'class-ph-admin-profile.php' );
710 break;
711 }
712 }
713
714 /**
715 * Include admin files conditionally
716 */
717 public function disable_propertyhive_meta_box_dragging()
718 {
719 $screen = get_current_screen();
720
721 if ( in_array( $screen->id, ph_get_screen_ids() ) )
722 {
723 //wp_deregister_script('postbox');
724 }
725 }
726
727 /**
728 * Remove PropertyHive meta boxes
729 */
730 public function remove_propertyhive_meta_boxes_from_screen_options()
731 {
732 global $wp_meta_boxes;
733
734 $screen = get_current_screen();
735
736 if ( in_array( $screen->id, array( 'property' ) ) )
737 {
738 //wp_deregister_script('postbox');
739 }
740 }
741
742 public function review_admin_notices()
743 {
744 global $wpdb;
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
755 if ( current_user_can( 'manage_options' ) )
756 {
757 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
758 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
759 {
760 if ( $propertyhive_review_prompt_due_timestamp < time() )
761 {
762 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
763 <p>
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' ) ) . "
765 </p>
766 <p>
767 <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
768 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
769 </p>
770 </div>";
771 }
772 }
773
774 if (
775 class_exists('Easy_Property_Listings') &&
776 ! $plugin_status_present &&
777 get_option( 'epl_notice_dismissed', '' ) != 'yes'
778 )
779 {
780 echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\">
781 <p>
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' ) ) . "
783 </p>
784 <p>
785 <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a>
786 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a>
787 </p>
788
789 </div>";
790 }
791
792 if (
793 !class_exists('PH_Demo_Data') &&
794 get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 &&
795 get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' &&
796 (
797 ! $admin_page_present
798 ||
799 (
800 $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
801 )
802 )
803 )
804 {
805 echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\">
806 <p>
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' ) ) . "
808 </p>
809 <p>
810 <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a>
811 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a>
812 </p>
813
814 </div>";
815 }
816
817 if (
818 get_option('propertyhive_search_results_page_id', '') == '' &&
819 (
820 ! $admin_page_present
821 ||
822 (
823 $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
824 )
825 ) &&
826 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
827 )
828 {
829 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
830 <p>
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' ) . "
832 </p>
833 <p>
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>
835 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
836 </p>
837
838 </div>";
839 }
840
841 if (
842 get_option('propertyhive_maps_provider') !== 'osm' &&
843 get_option('propertyhive_maps_provider') !== 'mapbox' &&
844 get_option('propertyhive_google_maps_api_key', '') == '' &&
845 ! $maps_api_key_submitted &&
846 (
847 ! $admin_page_present
848 ||
849 (
850 $admin_page_present && 'ph-installed' !== $admin_page
851 )
852 ) &&
853 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
854 )
855 {
856 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
857 <p>
858 " . sprintf(
859 /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
860 wp_kses_post( __( 'We noticed that you haven\'t entered a Google Maps API key. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="%s">enter it</a>.', 'propertyhive' ) ),
861 esc_url( admin_url('admin.php?page=ph-settings&tab=general&section=map') )
862 ) . "
863 </p>
864 <p>
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>
866 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
867 </p>
868
869 </div>";
870 }
871
872 if (
873 get_option('propertyhive_license_key', '') != '' &&
874 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
875 (
876 ! $admin_page_present
877 ||
878 (
879 $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
880 )
881 )
882 )
883 {
884 $license = PH()->license->get_current_license();
885 $output = '';
886
887 if ( isset($license['active']) && $license['active'] != '1' )
888 {
889 $output = __( 'Your Property Hive license key is inactive.', 'propertyhive' );
890 }
891 else
892 {
893
894 }
895
896 if ( $output != '' )
897 {
898 echo "<div class=\"notice notice-info\" id=\"ph_notice_invalid_expired_license_key\">
899 <p>
900 " . esc_html($output) . "
901 </p>
902 <p>
903 <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>
904 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
905 </p>
906
907 </div>";
908 }
909 }
910
911 $screen = get_current_screen();
912 if ( in_array( $screen->id, array( 'dashboard' ) ) )
913 {
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.
916 $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
917 $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
918 if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
919 {
920 echo '
921 <div class="notice notice-error" id="ph_notice_email_cron_not_running">
922 <p>' . esc_html(__( 'The Property Hive email queue does not appear to be running', 'propertyhive' )) . '
923 </p>
924 <p>
925 <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>
926 </p>
927 </div>
928 ';
929 }
930 }
931 }
932
933 if ( isset( $request_get['propertyhive_contacts_merged'] ) )
934 {
935 echo '
936 <div class="notice notice-info">
937 <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
938 </div>
939 ';
940 }
941 }
942
943 /**
944 * Handle redirects to welcome page after install.
945 */
946 public function admin_redirects()
947 {
948 // Setup wizard redirect
949 if ( get_transient( '_ph_activation_redirect' ) )
950 {
951 delete_transient( '_ph_activation_redirect' );
952
953 // Don't do redirect if part of multisite, doing batch-activate, or if no permission
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' ) ) {
956 return;
957 }
958
959 wp_safe_redirect( admin_url( 'index.php?page=ph-onboarding' ) );
960 exit;
961 }
962 }
963
964 public function prevent_access_to_admin()
965 {
966 global $current_user;
967
968 $user_roles = $current_user->roles;
969 $user_role = array_shift($user_roles);
970
971 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
972 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
973 {
974 wp_safe_redirect( home_url( '/' ) );
975 exit;
976 }
977 }
978
979 /**
980 * View previously sent email
981 *
982 * @return string
983 */
984 public function view_email() {
985
986 global $wpdb;
987
988 if ( isset( $_GET['view_propertyhive_email'] ) )
989 {
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' ) )
994 {
995 wp_die( 'Security check' );
996 }
997
998 if ( ! current_user_can( 'manage_propertyhive' ) )
999 {
1000 wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1001 }
1002
1003 if ( isset( $_GET['email_id'] ) )
1004 {
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 ) );
1008 if ( null !== $email_log )
1009 {
1010 $body = $email_log->body;
1011
1012 if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
1013 {
1014 $body = gzuncompress($body);
1015 }
1016
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;
1021
1022 }
1023 else
1024 {
1025 die("Email not found");
1026 }
1027 }
1028
1029 exit;
1030 }
1031 }
1032
1033 /**
1034 * Preview email template.
1035 *
1036 * @return string
1037 */
1038 public function preview_emails() {
1039 if ( isset( $_GET['preview_propertyhive_email'] ) )
1040 {
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' ) )
1045 {
1046 die( 'Security check' );
1047 }
1048
1049 $current_user = wp_get_current_user();
1050 $request_get = wp_unslash( $_GET );
1051 $request_post = wp_unslash( $_POST );
1052
1053 // get the preview email content
1054 $email_property_ids = array();
1055 if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
1056 {
1057 $email_property_ids = array( absint( $request_get['property_id'] ) );
1058 }
1059 elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
1060 {
1061 $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
1062 }
1063
1064 $allowed_tags = array(
1065 'strong' => array(),
1066 'span' => array(),
1067 'em' => array(),
1068 'h1' => array(),
1069 'h2' => array(),
1070 'h3' => array(),
1071 'h4' => array(),
1072 'h5' => array(),
1073 'h6' => array(),
1074 'i' => array(),
1075 'u' => array(),
1076 'b' => array(),
1077 'a' => array(
1078 'href' => array(),
1079 'target' => array(),
1080 ),
1081 );
1082 $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
1083
1084 $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1085 $body = wp_kses( $raw_body, $allowed_tags );
1086
1087 if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
1088 {
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 );
1092 }
1093 $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body );
1094
1095 $office_counts = array();
1096
1097 if ( strpos($body, '[properties]') !== FALSE )
1098 {
1099 ob_start();
1100
1101 if ( !empty($email_property_ids) )
1102 {
1103 foreach ( $email_property_ids as $email_property_id )
1104 {
1105 $property = new PH_Property((int)$email_property_id);
1106
1107 if ( $property->office_id != '' && $property->office_id != 0 )
1108 {
1109 if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
1110 ++$office_counts[$property->office_id];
1111 }
1112
1113 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
1114 }
1115 }
1116 $body = str_replace("[properties]", ob_get_clean(), $body);
1117 }
1118
1119 $office_name = '';
1120 $office_email_address = '';
1121
1122 $office_id = get_user_meta($current_user->ID, 'office_id', TRUE);
1123 if ($office_id == '')
1124 {
1125 // No office against user. Use email address of office with most properties
1126 if ( !empty($office_counts) )
1127 {
1128 arsort($office_counts);
1129 reset($office_counts);
1130 $office_id = key($office_counts);
1131 }
1132 }
1133
1134 if ( !empty($office_id) )
1135 {
1136 $office_name = get_the_title( (int) $office_id );
1137 $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE );
1138 }
1139
1140 $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
1141 $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
1142
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 );
1145
1146 // wrap the content with the email template and then add styles
1147 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1148
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.
1151 echo $message;
1152 exit;
1153 }
1154 }
1155 }
1156
1157 return new PH_Admin();
1158