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 +827 -37 1.4.52.3.0 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 if ( ! defined( 'ABSPATH' ) ) {
3 6 exit; // Exit if accessed directly
4 7 }
5 8
@@ -11,8 +14,9 @@
11 14 * @category Admin
12 15 * @package PropertyHive/Admin
13 16 * @version 1.0.0
14 17 */
18 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin; preserving the existing PH_* class name is required for plugin and extension compatibility.
15 19 class PH_Admin {
16 20
17 21 /**
18 22 * Constructor
@@ -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 ( $_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 - $_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 /**
@@ -189,8 +697,17 @@
189 697 switch ( $screen->id ) {
190 698 case 'dashboard' :
191 699 include( 'class-ph-admin-dashboard.php' );
192 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;
193 710 }
194 711 }
195 712
196 713 /**
@@ -222,8 +739,19 @@
222 739 }
223 740
224 741 public function review_admin_notices()
225 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 +
226 754 if ( current_user_can( 'manage_options' ) )
227 755 {
228 756 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
229 757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -229,22 +757,187 @@
229 757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
230 758 {
231 759 if ( $propertyhive_review_prompt_due_timestamp < time() )
232 760 {
233 - echo "<div class=\"notice notice-info is-dismissible\" id=\"ph_leave_review_admin_notice\">
761 + echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
234 762 <p>
235 - " . __( '<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' ) ) . "
236 764 </p>
237 765 <p>
238 - <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
239 - <a href=\"\" class=\"button\">No Thanks</a>
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>
240 768 </p>
241 769 </div>";
770 + }
771 + }
242 772
243 - update_option( 'propertyhive_review_prompt_due_timestamp', 0 );
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' );
244 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 + }
245 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 + }
246 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 + }
247 940 }
248 941
249 942 /**
250 943 * Handle redirects to welcome page after install.
@@ -256,8 +949,9 @@
256 949 {
257 950 delete_transient( '_ph_activation_redirect' );
258 951
259 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.
260 954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
261 955 return;
262 956 }
263 957
@@ -275,9 +969,10 @@
275 969
276 970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
277 971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
278 972 {
279 - exit( wp_redirect( home_url( '/' ) ) );
973 + wp_safe_redirect( home_url( '/' ) );
974 + exit;
280 975 }
281 976 }
282 977
283 978 /**
@@ -290,19 +985,39 @@
290 985 global $wpdb;
291 986
292 987 if ( isset( $_GET['view_propertyhive_email'] ) )
293 988 {
294 - 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' ) )
295 993 {
296 - die( 'Security check' );
994 + wp_die( 'Security check' );
297 995 }
298 996
997 + if ( ! current_user_can( 'manage_propertyhive' ) )
998 + {
999 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 + }
1001 +
299 1002 if ( isset( $_GET['email_id'] ) )
300 1003 {
301 - $email_log = $wpdb->get_row( "SELECT * FROM " . $wpdb->prefix . "ph_email_log WHERE email_id = '" . esc_sql( $_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 ) );
302 1007 if ( null !== $email_log )
303 1008 {
304 - 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;
305 1020
306 1021 }
307 1022 else
308 1023 {
@@ -321,30 +1036,80 @@
321 1036 */
322 1037 public function preview_emails() {
323 1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
324 1039 {
325 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) )
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' ) )
326 1044 {
327 1045 die( 'Security check' );
328 1046 }
329 1047
1048 + $current_user = wp_get_current_user();
1049 + $request_get = wp_unslash( $_GET );
1050 + $request_post = wp_unslash( $_POST );
1051 +
330 1052 // get the preview email content
331 - $email_property_ids = explode(",", $_POST['email_property_id']);
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 + }
332 1062
333 - $body = stripslashes($_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 );
334 1082
335 - $body = str_replace("[contact_name]", get_the_title($_GET['contact_id']), $body);
336 - $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 );
337 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 +
338 1096 if ( strpos($body, '[properties]') !== FALSE )
339 1097 {
340 1098 ob_start();
341 -
1099 +
342 1100 if ( !empty($email_property_ids) )
343 1101 {
344 1102 foreach ( $email_property_ids as $email_property_id )
345 1103 {
346 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 +
347 1112 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
348 1113 }
349 1114 }
350 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -349,15 +1114,40 @@
349 1114 }
350 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
351 1116 }
352 1117
353 - // create a new email
354 - $email = new PH_Emails();
1118 + $office_name = '';
1119 + $office_email_address = '';
355 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 +
356 1145 // wrap the content with the email template and then add styles
357 - $message = apply_filters( 'propertyhive_mail_content', $email->style_inline( $email->wrap_message( $body ) ) );
1146 + $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
358 1147
359 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.
360 1150 echo $message;
361 1151 exit;
362 1152 }
363 1153 }
@@ -362,5 +1152,5 @@
362 1152 }
363 1153 }
364 1154 }
365 1155
366 -return new PH_Admin();
1156 +return new PH_Admin();