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 +742 -70 1.4.602.3.0 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 if ( ! defined( 'ABSPATH' ) ) {
3 6 exit; // Exit if accessed directly
4 7 }
5 8
@@ -11,8 +14,9 @@
11 14 * @category Admin
12 15 * @package PropertyHive/Admin
13 16 * @version 1.0.0
14 17 */
18 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin; preserving the existing PH_* class name is required for plugin and extension compatibility.
15 19 class PH_Admin {
16 20
17 21 /**
18 22 * Constructor
@@ -23,8 +27,9 @@
23 27 add_action( 'current_screen', array( $this, 'conditional_includes' ) );
24 28 add_action( 'current_screen', array( $this, 'disable_propertyhive_meta_box_dragging' ) );
25 29 add_action( 'current_screen', array( $this, 'remove_propertyhive_meta_boxes_from_screen_options' ) );
26 30 add_action( 'admin_notices', array( $this, 'review_admin_notices') );
31 + add_action( 'admin_notices', array( $this, 'archive_admin_notices' ) );
27 32 add_action( 'admin_menu', array( $this, 'admin_dashboard_pages' ) );
28 33 add_action( 'admin_head', array( $this, 'admin_head' ) );
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
@@ -29,15 +34,515 @@
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
31 36 add_action( 'admin_init', array( $this, 'view_email' ) );
32 37 add_action( 'admin_init', array( $this, 'preview_emails' ) );
38 + add_action( 'admin_init', array( $this, 'record_recently_viewed' ) );
39 + add_action( 'admin_init', array( $this, 'export_applicant_list' ) );
40 + add_action( 'admin_init', array( $this, 'export_sub_grid' ) );
41 + add_action( 'admin_init', array( $this, 'check_hide_demo_data_tab' ) );
42 + add_action( 'admin_init', array( $this, 'check_install_add_on' ) );
43 + add_filter( 'propertyhive_screen_ids', array( $this, 'crm_only_mode_screen_id' ) );
33 44 }
45 +
46 + public function archive_admin_notices()
47 + {
48 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
49 + if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts']))
50 + {
51 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
52 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
53 + if ( $post_type )
54 + {
55 + $post_type_object = get_post_type_object($post_type);
56 + if ( ! $post_type_object ) {
57 + return;
58 + }
59 +
60 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
61 + $count = is_string($_GET['bulk_archived_posts']) ? absint($_GET['bulk_archived_posts']) : 0;
62 +
63 + if ( $post_type_object )
64 + {
65 + $message = sprintf(
66 + /* translators: 1: number of items, 2: post type label */
67 + _n(
68 + '%1$s %2$s moved to archive.',
69 + '%1$s %2$s moved to archive.',
70 + $count,
71 + 'propertyhive'
72 + ),
73 + number_format_i18n( $count ),
74 + $count === 1
75 + ? $post_type_object->labels->singular_name
76 + : $post_type_object->labels->name
77 + );
78 +
79 + printf(
80 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
81 + esc_html( $message )
82 + );
83 + }
84 + }
85 + }
86 +
87 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
88 + if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) )
89 + {
90 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
91 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
92 + if ( $post_type )
93 + {
94 + $post_type_object = get_post_type_object($post_type);
95 + if ( ! $post_type_object ) {
96 + return;
97 + }
98 +
99 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
100 + $count = is_string($_GET['bulk_unarchived_posts']) ? absint($_GET['bulk_unarchived_posts']) : 0;
101 +
102 + if ( $post_type_object )
103 + {
104 + $message = sprintf(
105 + /* translators: 1: number of items, 2: post type label */
106 + _n(
107 + '%1$s %2$s removed from archive.',
108 + '%1$s %2$s removed from archive.',
109 + $count,
110 + 'propertyhive'
111 + ),
112 + number_format_i18n( $count ),
113 + $count === 1
114 + ? $post_type_object->labels->singular_name
115 + : $post_type_object->labels->name
116 + );
117 +
118 + printf(
119 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
120 + esc_html( $message )
121 + );
122 + }
123 + }
124 + }
125 + }
126 +
127 + public function crm_only_mode_screen_id( $screen_ids )
128 + {
129 + $current_user = wp_get_current_user();
130 +
131 + $user_id = $current_user->ID;
132 +
133 + $crm_only_mode = get_user_meta( $user_id, 'crm_only_mode', TRUE );
134 +
135 + if ( $crm_only_mode == '1' )
136 + {
137 + $screen_ids[] = 'toplevel_page_ph-settings';
138 + }
139 +
140 + return $screen_ids;
141 + }
142 +
143 + public function check_install_add_on()
144 + {
145 + $request_get = wp_unslash( $_GET );
146 + $ph_action = isset( $request_get['ph_action'] ) && is_string( $request_get['ph_action'] ) ? sanitize_key( $request_get['ph_action'] ) : '';
147 + $encoded_slug = isset( $request_get['ph_add_on_slug'] ) && is_string( $request_get['ph_add_on_slug'] ) ? sanitize_text_field( $request_get['ph_add_on_slug'] ) : '';
148 + $encoded_plugin = isset( $request_get['ph_add_on_plugin'] ) && is_string( $request_get['ph_add_on_plugin'] ) ? sanitize_text_field( $request_get['ph_add_on_plugin'] ) : '';
149 +
150 + if ( 'install_add_on' === $ph_action && '' !== $encoded_slug && '' !== $encoded_plugin )
151 + {
152 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) ) {
153 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
154 + }
155 + check_admin_referer( 'propertyhive-install-add-on' );
156 +
157 + $installed_plugins = get_option( 'propertyhive_pre_pro_add_ons', array());
158 +
159 + if ( empty($installed_plugins) )
160 + {
161 + $installed_plugins = array();
162 + }
163 +
164 + $decoded_slug = base64_decode( $encoded_slug, true );
165 + $decoded_plugin = base64_decode( $encoded_plugin, true );
166 + if ( false === $decoded_slug || false === $decoded_plugin ) {
167 + wp_die( esc_html__( 'Invalid add-on request.', 'propertyhive' ), '', array( 'response' => 400 ) );
168 + }
169 +
170 + $installed_plugins[] = array(
171 + 'slug' => ph_clean( $decoded_slug ),
172 + 'plugin' => ph_clean( $decoded_plugin )
173 + );
174 +
175 + update_option( 'propertyhive_pre_pro_add_ons', $installed_plugins );
176 +
177 + wp_safe_redirect( admin_url('admin.php?page=ph-settings&tab=features') );
178 + die();
179 + }
180 + }
181 +
182 + public function check_hide_demo_data_tab()
183 + {
184 + $request_get = wp_unslash( $_GET );
185 + $tab = isset( $request_get['tab'] ) && is_string( $request_get['tab'] ) ? sanitize_key( $request_get['tab'] ) : '';
186 + $hide_tab = isset( $request_get['hidetab'] ) && is_scalar( $request_get['hidetab'] ) ? (string) $request_get['hidetab'] : '';
187 +
188 + if ( 'demo_data' === $tab && '' !== $hide_tab )
189 + {
190 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
191 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
192 + }
193 + check_admin_referer( 'propertyhive-hide-demo-data' );
194 +
195 + update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
196 + wp_safe_redirect( admin_url('admin.php?page=ph-settings') );
197 + die();
198 + }
199 + }
200 +
201 + public function export_sub_grid()
202 + {
203 + $request_get = wp_unslash( $_GET );
204 + $sub_grid = isset( $request_get['sub_grid'] ) && is_string( $request_get['sub_grid'] ) ? sanitize_key( $request_get['sub_grid'] ) : '';
205 + $raw_record_ids = isset( $request_get['record_ids'] ) && is_string( $request_get['record_ids'] ) ? sanitize_text_field( $request_get['record_ids'] ) : '';
206 +
207 + if ( '' !== $sub_grid )
208 + {
209 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
210 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
211 + }
212 + check_admin_referer( 'propertyhive-export-sub-grid', 'ph_export_nonce' );
213 +
214 + $export_types = array(
215 + 'property-viewings-grid' => 'viewing',
216 + 'contact-viewings-grid' => 'viewing',
217 + 'property-offers-grid' => 'offer',
218 + 'contact-offers-grid' => 'offer',
219 + 'property-sales-grid' => 'sale',
220 + 'contact-sales-grid' => 'sale',
221 + );
222 + $record_ids = '' !== $raw_record_ids
223 + ? array_values( array_filter( array_map( 'absint', explode( '|', $raw_record_ids ) ) ) )
224 + : array();
225 +
226 + if ( ! isset( $export_types[ $sub_grid ] ) || empty( $record_ids ) ) {
227 + wp_die( esc_html__( 'Invalid export request', 'propertyhive' ), '', array( 'response' => 400 ) );
228 + }
229 + foreach ( $record_ids as $record_id ) {
230 + if ( get_post_type( $record_id ) !== $export_types[ $sub_grid ] || ! current_user_can( 'edit_post', $record_id ) ) {
231 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
232 + }
233 + }
234 +
235 + ob_start();
236 +
237 + $df = fopen("php://output", 'w');
238 +
239 + $columns = array( 'id' => __( 'ID', 'propertyhive' ) );
240 +
241 + if ( strpos( $sub_grid, 'viewings' ) !== false )
242 + {
243 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
244 + $columns['property'] = __( 'Property', 'propertyhive' );
245 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
246 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
247 + $columns['negotiator'] = __( 'Attending Negotiator(s)', 'propertyhive' );
248 + $columns['status'] = __( 'Status', 'propertyhive' );
249 + $columns['feedback'] = __( 'Feedback', 'propertyhive' );
250 + }
251 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
252 + {
253 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
254 + $columns['property'] = __( 'Property', 'propertyhive' );
255 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
256 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
257 + $columns['status'] = __( 'Status', 'propertyhive' );
258 + $columns['amount'] = __( 'Offer Amount', 'propertyhive' );
259 + }
260 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
261 + {
262 + $columns['date'] = __( 'Date', 'propertyhive' );
263 + $columns['property'] = __( 'Property', 'propertyhive' );
264 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
265 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
266 + $columns['status'] = __( 'Status', 'propertyhive' );
267 + $columns['amount'] = __( 'Sale Amount', 'propertyhive' );
268 + }
269 +
270 + fputcsv($df, $columns);
271 +
272 + if ( ! empty( $record_ids ) )
273 + {
274 + if ( !empty($record_ids) )
275 + {
276 + if ( strpos( $sub_grid, 'viewings' ) !== false )
277 + {
278 + $args = array(
279 + 'post_type' => 'viewing',
280 + 'nopaging' => TRUE,
281 + 'fields' => 'ids',
282 + 'post__in' => $record_ids,
283 + 'order' => 'ASC',
284 + 'orderby' => 'meta_value',
285 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked viewing list by its fixed date-time metadata key.
286 + 'meta_key' => '_start_date_time',
287 + );
288 +
289 + $records_query = new WP_Query( $args );
290 +
291 + if ( $records_query->have_posts() )
292 + {
293 + while ( $records_query->have_posts() )
294 + {
295 + $records_query->the_post();
296 +
297 + $viewing = new PH_Viewing( get_the_ID() );
298 +
299 + $property_id = (int)$viewing->_property_id;
300 + $property_address = '';
301 + if ( !empty($property_id) )
302 + {
303 + $property = new PH_Property( $property_id );
304 + $property_address = $property->get_formatted_full_address();
305 + }
306 +
307 + $columns = array(
308 + get_the_ID(),
309 + gmdate("H:i jS F Y", strtotime($viewing->_start_date_time)),
310 + $property_address,
311 + str_replace("<br>", "\n", $viewing->get_applicants()),
312 + $viewing->get_negotiators(),
313 + str_replace("<br>", "\n", $viewing->get_status()),
314 + $viewing->_feedback
315 + );
316 +
317 + fputcsv($df, $columns);
318 + }
319 + }
320 + }
321 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
322 + {
323 + $args = array(
324 + 'post_type' => 'offer',
325 + 'nopaging' => TRUE,
326 + 'fields' => 'ids',
327 + 'post__in' => $record_ids,
328 + 'order' => 'ASC',
329 + 'orderby' => 'meta_value',
330 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked offer list by its fixed date-time metadata key.
331 + 'meta_key' => '_offer_date_time',
332 + );
333 +
334 + $records_query = new WP_Query( $args );
335 +
336 + if ( $records_query->have_posts() )
337 + {
338 + while ( $records_query->have_posts() )
339 + {
340 + $records_query->the_post();
341 +
342 + $offer = new PH_Offer( get_the_ID() );
343 +
344 + $property_id = (int)$offer->_property_id;
345 + $property_address = '';
346 + if ( !empty($property_id) )
347 + {
348 + $property = new PH_Property( $property_id );
349 + $property_address = $property->get_formatted_full_address();
350 + }
351 +
352 + $columns = array(
353 + get_the_ID(),
354 + gmdate("H:i jS F Y", strtotime($offer->_offer_date_time)),
355 + $property_address,
356 + str_replace("<br>", "\n", $offer->get_applicants()),
357 + $offer->_status,
358 + html_entity_decode($offer->get_formatted_amount())
359 + );
360 +
361 + fputcsv($df, $columns);
362 + }
363 + }
364 + }
365 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
366 + {
367 + $args = array(
368 + 'post_type' => 'sale',
369 + 'nopaging' => TRUE,
370 + 'fields' => 'ids',
371 + 'post__in' => $record_ids,
372 + 'order' => 'ASC',
373 + 'orderby' => 'meta_value',
374 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked sale list by its fixed date-time metadata key.
375 + 'meta_key' => '_sale_date_time',
376 + );
377 +
378 + $records_query = new WP_Query( $args );
379 +
380 + if ( $records_query->have_posts() )
381 + {
382 + while ( $records_query->have_posts() )
383 + {
384 + $records_query->the_post();
385 +
386 + $sale = new PH_Sale( get_the_ID() );
387 +
388 + $property_id = (int)$sale->_property_id;
389 + $property_address = '';
390 + if ( !empty($property_id) )
391 + {
392 + $property = new PH_Property( $property_id );
393 + $property_address = $property->get_formatted_full_address();
394 + }
395 +
396 + $columns = array(
397 + get_the_ID(),
398 + gmdate("jS F Y", strtotime($sale->_sale_date_time)),
399 + $property_address,
400 + str_replace("<br>", "\n", $sale->get_applicants()),
401 + $sale->_status,
402 + html_entity_decode($sale->get_formatted_amount())
403 + );
404 +
405 + fputcsv($df, $columns);
406 + }
407 + }
408 + }
409 + }
410 + }
411 +
412 + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
413 +
414 + $output = ob_get_clean();
415 +
416 + $filename = sanitize_title( $sub_grid ) . '-' . gmdate("YmdHis") . '.csv';
417 +
418 + // disable caching
419 + $now = gmdate("D, d M Y H:i:s");
420 + header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
421 + header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
422 + header("Last-Modified: {$now} GMT");
423 +
424 + // force download
425 + header("Content-Type: application/force-download");
426 + header("Content-Type: application/octet-stream");
427 + header("Content-Type: application/download");
428 +
429 + // disposition / encoding on response body
430 + header("Content-Disposition: attachment;filename={$filename}");
431 + header("Content-Transfer-Encoding: binary");
432 +
433 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download produced by fputcsv, not HTML; HTML escaping would corrupt exported field values.
434 + echo $output;
435 +
436 + die();
437 + }
438 + }
439 +
440 + public function export_applicant_list()
441 + {
442 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
443 + $request_post = wp_unslash( $_POST );
444 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
445 + $submitted_applicant_list = isset( $request_post['submitted_applicant_list'] ) && '1' === (string) $request_post['submitted_applicant_list'];
446 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
447 + $export_applicant_list_results = isset( $request_post['export_applicant_list_results'] ) && '1' === (string) $request_post['export_applicant_list_results'];
448 +
449 + if ( $submitted_applicant_list && $export_applicant_list_results )
450 + {
451 + include_once( 'class-ph-admin-applicant-list.php' );
452 + $ph_admin_applicant_list = new PH_Admin_Applicant_List();
453 + $ph_admin_applicant_list->export();
454 + }
455 + }
456 +
457 + public function record_recently_viewed()
458 + {
459 + global $pagenow;
460 +
461 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
462 + $request_get = wp_unslash( $_GET );
463 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
464 + $recent_post_id = isset( $request_get['post'] ) && is_scalar( $request_get['post'] ) ? absint( $request_get['post'] ) : 0;
465 +
466 + if (
467 + 'post.php' === $pagenow &&
468 + $recent_post_id > 0 &&
469 + in_array(
470 + get_post_type( $recent_post_id ),
471 + apply_filters( 'propertyhive_post_types_with_tabs', array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale') )
472 + )
473 + )
474 + {
475 + $recently_viewed = get_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', TRUE );
476 +
477 + if ( !is_array($recently_viewed) )
478 + {
479 + $recently_viewed = array();
480 + }
481 +
482 + foreach ( $recently_viewed as $time => $post )
483 + {
484 + if ( $recent_post_id == $post['id'] )
485 + {
486 + unset($recently_viewed[$time]);
487 + }
488 + }
489 +
490 + $title = get_the_title( $recent_post_id );
491 +
492 + switch ( get_post_type( $recent_post_id ) )
493 + {
494 + case "appraisal":
495 + {
496 + $appraisal = new PH_Appraisal( $recent_post_id );
497 + $title = $appraisal->get_formatted_summary_address();
498 + break;
499 + }
500 + case "property":
501 + {
502 + $property = new PH_Property( $recent_post_id );
503 + $title = $property->get_formatted_summary_address();
504 + break;
505 + }
506 + case "enquiry":
507 + case "viewing":
508 + case "offer":
509 + case "sale":
510 + {
511 + $property_id = get_post_meta( $recent_post_id, '_property_id', TRUE );
512 + if ( $property_id != '' )
513 + {
514 + $property = new PH_Property( (int)$property_id );
515 + $title = $property->get_formatted_summary_address();
516 + }
517 + break;
518 + }
519 + }
520 +
521 + $title = ucfirst( get_post_type( $recent_post_id ) ) . ' - ' . $title;
522 +
523 + $recently_viewed = array(time() => array(
524 + 'id' => $recent_post_id,
525 + 'title' => $title,
526 + 'post_type' => get_post_type( $recent_post_id ),
527 + 'edit_link' => get_edit_post_link( $recent_post_id ),
528 + )) + $recently_viewed;
529 +
530 + $recently_viewed = array_slice($recently_viewed, 0, 10, TRUE);
531 +
532 + update_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', $recently_viewed );
533 + }
534 + }
34 535
35 536 public function admin_dashboard_pages()
36 537 {
37 - if ( ! empty( $_GET['page'] ) )
538 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This registers a read-only admin dashboard page and does not change state.
539 + $request_get = wp_unslash( $_GET );
540 + $admin_page = isset( $request_get['page'] ) && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
541 +
542 + if ( '' !== $admin_page )
38 543 {
39 - switch ( sanitize_title($_GET['page']) )
544 + switch ( $admin_page )
40 545 {
41 546 case 'ph-installed':
42 547 {
43 548 add_dashboard_page(
@@ -43,9 +548,9 @@
43 548 add_dashboard_page(
44 549 __( 'Welcome to Property Hive', 'propertyhive' ),
45 550 __( 'Welcome to Property Hive', 'propertyhive' ),
46 551 'manage_propertyhive',
47 - sanitize_title($_GET['page']),
552 + $admin_page,
48 553 array( $this, 'installed_screen' )
49 554 );
50 555
51 556 break;
@@ -58,9 +563,9 @@
58 563 {
59 564 ?>
60 565 <div class="wrap propertyhive-installed-screen">
61 566
62 - <h1><?php _e( 'Welcome to Property Hive', 'propertyhive' ); ?></h1>
567 + <h1><?php echo esc_html(__( 'Welcome to Property Hive', 'propertyhive' )); ?></h1>
63 568
64 569 <div class="intro-text">
65 570 <p>Thank you choosing Property Hive to power your next property website. Below you'll find useful links, tips on getting started, and more.</p>
66 571 </div>
@@ -72,13 +577,13 @@
72 577 <h2>Getting Started</h2>
73 578
74 579 <p>Now that you've installed Property Hive you'll notice a new 'Property Hive' item in the left hand menu of WordPress.</p>
75 580
76 - <img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
581 + <img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
77 582
78 - <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo admin_url( 'admin.php?page=ph-settings' ); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
583 + <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo esc_url(admin_url( 'admin.php?page=ph-settings' )); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
79 584
80 - <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo admin_url( 'post-new.php?post_type=property' ); ?>" target="_blank">adding your first property</a>.</p>
585 + <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo esc_url(admin_url( 'post-new.php?post_type=property' )); ?>" target="_blank">adding your first property</a>.</p>
81 586
82 587 </div>
83 588
84 589 <div class="panel">
@@ -86,9 +591,9 @@
86 591 <h2>Extending Property Hive</h2>
87 592
88 593 <p>We have a <a href="https://wp-property-hive.com/add-ons/" target="_blank">wide range of add ons</a> available to add extra functionality to your website.</p>
89 594
90 - <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
595 + <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
91 596
92 597 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=free" target="_blank">Free Add Ons</a></strong><br>
93 598 From our template assistant add on to a variety of calculators, these free add ons are great additions to any property website.</p>
94 599
@@ -97,10 +602,10 @@
97 602
98 603 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=tools" target="_blank">Internal Tools</a></strong><br>
99 604 Add ons aimed to make your life easier and to save you time. Includes Digital Window Displays, Address Lookup and more.</p>
100 605
101 - <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import-export" target="_blank">Import and Export</a></strong><br>
102 - Send your properties to portals like Rightmove, Zoopla and more or import properties from thid party software. These add ons automate the import and export of property data.</p>
606 + <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import" target="_blank">Import and Export</a></strong><br>
607 + Import properties from third party software or send your properties to portals like Rightmove, Zoopla and more. These add ons automate the import and export of property data.</p>
103 608
104 609 </div>
105 610
106 611 <div class="panel">
@@ -109,13 +614,10 @@
109 614
110 615 We pride ourselves on great support at Property Hive and will always do what we can to help you make create the best site possible. Please find below some useful links relating to our support:
111 616
112 617 <p><strong style="font-size:14px;">Documentation</strong><br>
113 - We have documentation <a href="https://wp-property-hive.com/documentation/" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
618 + We have documentation <a href="https://docs.wp-property-hive.com" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
114 619
115 - <p><strong style="font-size:14px;">Priority One-To-One Support</strong><br>
116 - If you require help quickly, or wish to discuss a bespoke requirement, then <a href="https://wp-property-hive.com/product/12-month-license-key-subscription/" 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 /**
@@ -231,8 +739,19 @@
231 739 }
232 740
233 741 public function review_admin_notices()
234 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 +
235 754 if ( current_user_can( 'manage_options' ) )
236 755 {
237 756 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
238 757 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -240,12 +759,12 @@
240 759 if ( $propertyhive_review_prompt_due_timestamp < time() )
241 760 {
242 761 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
243 762 <p>
244 - " . __( '<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' ) ) . "
245 764 </p>
246 765 <p>
247 - <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
766 + <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
248 767 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
249 768 </p>
250 769 </div>";
251 770 }
@@ -251,14 +770,57 @@
251 770 }
252 771 }
253 772
254 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 (
255 817 get_option('propertyhive_search_results_page_id', '') == '' &&
256 818 (
257 - !isset($_GET['page'])
819 + ! $admin_page_present
258 820 ||
259 821 (
260 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
822 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
261 823 )
262 824 ) &&
263 825 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
264 826 )
@@ -264,13 +826,13 @@
264 826 )
265 827 {
266 828 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
267 829 <p>
268 - " . __( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
830 + " . esc_html__( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
269 831 </p>
270 832 <p>
271 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general') . "\" class=\"button-primary\">Go To Property Hive Settings</a>
272 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">Dismiss</a>
833 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To Property Hive Settings', 'propertyhive' )) . "</a>
834 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
273 835 </p>
274 836
275 837 </div>";
276 838 }
@@ -275,15 +837,17 @@
275 837 </div>";
276 838 }
277 839
278 840 if (
841 + get_option('propertyhive_maps_provider') !== 'osm' &&
842 + get_option('propertyhive_maps_provider') !== 'mapbox' &&
279 843 get_option('propertyhive_google_maps_api_key', '') == '' &&
280 - !isset($_POST['propertyhive_google_maps_api_key']) &&
844 + ! $maps_api_key_submitted &&
281 845 (
282 - !isset($_GET['page'])
846 + ! $admin_page_present
283 847 ||
284 848 (
285 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
849 + $admin_page_present && 'ph-installed' !== $admin_page
286 850 )
287 851 ) &&
288 852 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
289 853 )
@@ -289,13 +853,17 @@
289 853 )
290 854 {
291 855 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
292 856 <p>
293 - " . __( 'We noticed that you haven\'t entered a Google Maps API key yet. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="'. admin_url('admin.php?page=ph-settings&tab=general&section=map') . '">enter it</a>.', 'propertyhive' ) . "
857 + " . sprintf(
858 + /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
859 + wp_kses_post( __( 'We noticed that you haven\'t entered a Google Maps API key. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="%s">enter it</a>.', 'propertyhive' ) ),
860 + esc_url( admin_url('admin.php?page=ph-settings&tab=general&section=map') )
861 + ) . "
294 862 </p>
295 863 <p>
296 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general&section=map') . "\" class=\"button-primary\">Enter Google Maps API Key</a>
297 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">Dismiss</a>
864 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general&section=map')) . "\" class=\"button-primary\">" . esc_html(__( 'Enter Google Maps API Key', 'propertyhive' )) . "</a>
865 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
298 866 </p>
299 867
300 868 </div>";
301 869 }
@@ -303,12 +871,12 @@
303 871 if (
304 872 get_option('propertyhive_license_key', '') != '' &&
305 873 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
306 874 (
307 - !isset($_GET['page'])
875 + ! $admin_page_present
308 876 ||
309 877 (
310 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
878 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
311 879 )
312 880 )
313 881 )
314 882 {
@@ -316,32 +884,13 @@
316 884 $output = '';
317 885
318 886 if ( isset($license['active']) && $license['active'] != '1' )
319 887 {
320 - $output = __( 'You\'re Property Hive license key is inactive.', 'propertyhive' );
888 + $output = __( 'Your Property Hive license key is inactive.', 'propertyhive' );
321 889 }
322 890 else
323 891 {
324 - if ( isset($license['expires_at']) && $license['expires_at'] != '' )
325 - {
326 - if ( strtotime($license['expires_at']) <= time() )
327 - {
328 - // Expired
329 - $output = __( 'Your Property Hive license key expired on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
330 - }
331 - elseif (
332 - strtotime($license['expires_at']) > time() &&
333 - strtotime($license['expires_at']) < (time() + 30 * 24 * 60 * 60)
334 - )
335 - {
336 - // Expires in less than 30 days
337 - $output = __( 'Your Property Hive license key expires on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
338 - }
339 - elseif (strtotime($license['expires_at']) > time())
340 - {
341 - // Valid
342 - }
343 - }
892 +
344 893 }
345 894
346 895 if ( $output != '' )
347 896 {
@@ -346,19 +895,49 @@
346 895 if ( $output != '' )
347 896 {
348 897 echo "<div class=\"notice notice-info\" id=\"ph_notice_invalid_expired_license_key\">
349 898 <p>
350 - " . $output . "
899 + " . esc_html($output) . "
351 900 </p>
352 901 <p>
353 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=licensekey') . "\" class=\"button-primary\">Go To License Key Settings</a>
354 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">Dismiss</a>
902 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=licensekey')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To License Key Settings', 'propertyhive' )) . "</a>
903 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
355 904 </p>
356 905
357 906 </div>";
358 907 }
359 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 + }
360 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 + }
361 940 }
362 941
363 942 /**
364 943 * Handle redirects to welcome page after install.
@@ -370,8 +949,9 @@
370 949 {
371 950 delete_transient( '_ph_activation_redirect' );
372 951
373 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.
374 954 if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
375 955 return;
376 956 }
377 957
@@ -389,9 +969,10 @@
389 969
390 970 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
391 971 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
392 972 {
393 - exit( wp_redirect( home_url( '/' ) ) );
973 + wp_safe_redirect( home_url( '/' ) );
974 + exit;
394 975 }
395 976 }
396 977
397 978 /**
@@ -404,19 +985,39 @@
404 985 global $wpdb;
405 986
406 987 if ( isset( $_GET['view_propertyhive_email'] ) )
407 988 {
408 - 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' ) )
409 993 {
410 - die( 'Security check' );
994 + wp_die( 'Security check' );
411 995 }
412 996
997 + if ( ! current_user_can( 'manage_propertyhive' ) )
998 + {
999 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1000 + }
1001 +
413 1002 if ( isset( $_GET['email_id'] ) )
414 1003 {
415 - $email_log = $wpdb->get_row( "SELECT * FROM " . $wpdb->prefix . "ph_email_log WHERE email_id = '" . esc_sql( (int)$_GET['email_id'] ) . "'" );
1004 + $email_id = is_string( $_GET['email_id'] ) ? absint( $_GET['email_id'] ) : 0;
1005 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Email logs are stored in a custom plugin table and this is a single protected administrative lookup.
1006 + $email_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ph_email_log WHERE email_id = %d", $email_id ) );
416 1007 if ( null !== $email_log )
417 1008 {
418 - 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;
419 1020
420 1021 }
421 1022 else
422 1023 {
@@ -435,28 +1036,64 @@
435 1036 */
436 1037 public function preview_emails() {
437 1038 if ( isset( $_GET['preview_propertyhive_email'] ) )
438 1039 {
439 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-applicants' ) )
1040 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
1041 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
1042 + }
1043 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-properties' ) && ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-applicants' ) )
440 1044 {
441 1045 die( 'Security check' );
442 1046 }
443 1047
1048 + $current_user = wp_get_current_user();
1049 + $request_get = wp_unslash( $_GET );
1050 + $request_post = wp_unslash( $_POST );
1051 +
444 1052 // get the preview email content
445 - if ( isset($_GET['property_id']) )
1053 + $email_property_ids = array();
1054 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
446 1055 {
447 - $email_property_ids = array((int)$_GET['property_id']);
1056 + $email_property_ids = array( absint( $request_get['property_id'] ) );
448 1057 }
449 - elseif ( isset($_POST['email_property_id']) )
1058 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
450 1059 {
451 - $email_property_ids = explode(",", sanitize_text_field($_POST['email_property_id']));
1060 + $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
452 1061 }
453 1062
454 - $body = stripslashes(sanitize_textarea_field($_POST['body']));
1063 + $allowed_tags = array(
1064 + 'strong' => array(),
1065 + 'span' => array(),
1066 + 'em' => array(),
1067 + 'h1' => array(),
1068 + 'h2' => array(),
1069 + 'h3' => array(),
1070 + 'h4' => array(),
1071 + 'h5' => array(),
1072 + 'h6' => array(),
1073 + 'i' => array(),
1074 + 'u' => array(),
1075 + 'b' => array(),
1076 + 'a' => array(
1077 + 'href' => array(),
1078 + 'target' => array(),
1079 + ),
1080 + );
1081 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
455 1082
456 - $body = str_replace("[contact_name]", get_the_title((int)$_GET['contact_id']), $body);
457 - $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 );
458 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 +
459 1096 if ( strpos($body, '[properties]') !== FALSE )
460 1097 {
461 1098 ob_start();
462 1099
@@ -464,8 +1101,15 @@
464 1101 {
465 1102 foreach ( $email_property_ids as $email_property_id )
466 1103 {
467 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 +
468 1112 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
469 1113 }
470 1114 }
471 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -470,12 +1114,40 @@
470 1114 }
471 1115 $body = str_replace("[properties]", ob_get_clean(), $body);
472 1116 }
473 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 +
474 1145 // wrap the content with the email template and then add styles
475 1146 $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
476 1147
477 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.
478 1150 echo $message;
479 1151 exit;
480 1152 }
481 1153 }
@@ -480,5 +1152,5 @@
480 1152 }
481 1153 }
482 1154 }
483 1155
484 -return new PH_Admin();
1156 +return new PH_Admin();