PluginProbe
Property Hive / 2.3.0
Property Hive v2.3.0
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 1.4.62 All 260 releases
propertyhive / includes / admin / class-ph-admin.php

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

1,157 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( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
675
676 // Classes we only need if the ajax is not-ajax
677 if ( ! is_ajax() ) {
678 include( 'class-ph-admin-menus.php' );
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 }
686 }
687 }
688
689 /**
690 * Include admin files conditionally.
691 */
692 public function conditional_includes() {
693 if ( ! $screen = get_current_screen() ) {
694 return;
695 }
696
697 switch ( $screen->id ) {
698 case 'dashboard' :
699 include( 'class-ph-admin-dashboard.php' );
700 break;
701 case 'plugins' :
702 include( 'class-ph-admin-plugin-updates.php' );
703 break;
704 case 'users':
705 case 'user':
706 case 'profile':
707 case 'user-edit':
708 include( 'class-ph-admin-profile.php' );
709 break;
710 }
711 }
712
713 /**
714 * Include admin files conditionally
715 */
716 public function disable_propertyhive_meta_box_dragging()
717 {
718 $screen = get_current_screen();
719
720 if ( in_array( $screen->id, ph_get_screen_ids() ) )
721 {
722 //wp_deregister_script('postbox');
723 }
724 }
725
726 /**
727 * Remove PropertyHive meta boxes
728 */
729 public function remove_propertyhive_meta_boxes_from_screen_options()
730 {
731 global $wp_meta_boxes;
732
733 $screen = get_current_screen();
734
735 if ( in_array( $screen->id, array( 'property' ) ) )
736 {
737 //wp_deregister_script('postbox');
738 }
739 }
740
741 public function review_admin_notices()
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
754 if ( current_user_can( 'manage_options' ) )
755 {
756 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
758 {
759 if ( $propertyhive_review_prompt_due_timestamp < time() )
760 {
761 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
762 <p>
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' ) ) . "
764 </p>
765 <p>
766 <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
767 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
768 </p>
769 </div>";
770 }
771 }
772
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 (
817 get_option('propertyhive_search_results_page_id', '') == '' &&
818 (
819 ! $admin_page_present
820 ||
821 (
822 $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
823 )
824 ) &&
825 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
826 )
827 {
828 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
829 <p>
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' ) . "
831 </p>
832 <p>
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>
835 </p>
836
837 </div>";
838 }
839
840 if (
841 get_option('propertyhive_maps_provider') !== 'osm' &&
842 get_option('propertyhive_maps_provider') !== 'mapbox' &&
843 get_option('propertyhive_google_maps_api_key', '') == '' &&
844 ! $maps_api_key_submitted &&
845 (
846 ! $admin_page_present
847 ||
848 (
849 $admin_page_present && 'ph-installed' !== $admin_page
850 )
851 ) &&
852 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
853 )
854 {
855 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
856 <p>
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 ) . "
862 </p>
863 <p>
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>
866 </p>
867
868 </div>";
869 }
870
871 if (
872 get_option('propertyhive_license_key', '') != '' &&
873 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
874 (
875 ! $admin_page_present
876 ||
877 (
878 $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
879 )
880 )
881 )
882 {
883 $license = PH()->license->get_current_license();
884 $output = '';
885
886 if ( isset($license['active']) && $license['active'] != '1' )
887 {
888 $output = __( 'Your Property Hive license key is inactive.', 'propertyhive' );
889 }
890 else
891 {
892
893 }
894
895 if ( $output != '' )
896 {
897 echo "<div class=\"notice notice-info\" id=\"ph_notice_invalid_expired_license_key\">
898 <p>
899 " . esc_html($output) . "
900 </p>
901 <p>
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>
904 </p>
905
906 </div>";
907 }
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 }
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 }
940 }
941
942 /**
943 * Handle redirects to welcome page after install.
944 */
945 public function admin_redirects()
946 {
947 // Setup wizard redirect
948 if ( get_transient( '_ph_activation_redirect' ) )
949 {
950 delete_transient( '_ph_activation_redirect' );
951
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.
954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
955 return;
956 }
957
958 wp_safe_redirect( admin_url( 'index.php?page=ph-installed' ) );
959 exit;
960 }
961 }
962
963 public function prevent_access_to_admin()
964 {
965 global $current_user;
966
967 $user_roles = $current_user->roles;
968 $user_role = array_shift($user_roles);
969
970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
972 {
973 wp_safe_redirect( home_url( '/' ) );
974 exit;
975 }
976 }
977
978 /**
979 * View previously sent email
980 *
981 * @return string
982 */
983 public function view_email() {
984
985 global $wpdb;
986
987 if ( isset( $_GET['view_propertyhive_email'] ) )
988 {
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' ) )
993 {
994 wp_die( 'Security check' );
995 }
996
997 if ( ! current_user_can( 'manage_propertyhive' ) )
998 {
999 wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 }
1001
1002 if ( isset( $_GET['email_id'] ) )
1003 {
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 ) );
1007 if ( null !== $email_log )
1008 {
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;
1020
1021 }
1022 else
1023 {
1024 die("Email not found");
1025 }
1026 }
1027
1028 exit;
1029 }
1030 }
1031
1032 /**
1033 * Preview email template.
1034 *
1035 * @return string
1036 */
1037 public function preview_emails() {
1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
1039 {
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' ) )
1044 {
1045 die( 'Security check' );
1046 }
1047
1048 $current_user = wp_get_current_user();
1049 $request_get = wp_unslash( $_GET );
1050 $request_post = wp_unslash( $_POST );
1051
1052 // get the preview email content
1053 $email_property_ids = array();
1054 if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
1055 {
1056 $email_property_ids = array( absint( $request_get['property_id'] ) );
1057 }
1058 elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
1059 {
1060 $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
1061 }
1062
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 );
1082
1083 $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1084 $body = wp_kses( $raw_body, $allowed_tags );
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
1096 if ( strpos($body, '[properties]') !== FALSE )
1097 {
1098 ob_start();
1099
1100 if ( !empty($email_property_ids) )
1101 {
1102 foreach ( $email_property_ids as $email_property_id )
1103 {
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
1112 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
1113 }
1114 }
1115 $body = str_replace("[properties]", ob_get_clean(), $body);
1116 }
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
1145 // wrap the content with the email template and then add styles
1146 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1147
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.
1150 echo $message;
1151 exit;
1152 }
1153 }
1154 }
1155
1156 return new PH_Admin();
1157