PluginProbe
Property Hive / 2.4.0
Property Hive v2.4.0
2.4.0 2.3.1 2.3.0 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 1.4.46 1.4.47 1.4.48 1.4.49 1.4.5 1.4.50 1.4.51 1.4.52 1.4.53 1.4.54 1.4.55 1.4.56 1.4.57 1.4.58 1.4.59 1.4.6 1.4.60 All 262 releases
← All changes | includes/admin/class-ph-admin.php +751 -75 1.4.482.4.0 View file →
@@ -1,5 +1,8 @@
1 1 <?php
2 +// phpcs:set WordPress.Security.ValidatedSanitizedInput customSanitizingFunctions[] ph_clean
3 +// ph_clean() recursively sanitizes text; presence, shape and unslashing checks remain separate.
4 +
2 5 if ( ! defined( 'ABSPATH' ) ) {
3 6 exit; // Exit if accessed directly
4 7 }
5 8
@@ -11,8 +14,9 @@
11 14 * @category Admin
12 15 * @package PropertyHive/Admin
13 16 * @version 1.0.0
14 17 */
18 +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Admin; preserving the existing PH_* class name is required for plugin and extension compatibility.
15 19 class PH_Admin {
16 20
17 21 /**
18 22 * Constructor
@@ -23,8 +27,9 @@
23 27 add_action( 'current_screen', array( $this, 'conditional_includes' ) );
24 28 add_action( 'current_screen', array( $this, 'disable_propertyhive_meta_box_dragging' ) );
25 29 add_action( 'current_screen', array( $this, 'remove_propertyhive_meta_boxes_from_screen_options' ) );
26 30 add_action( 'admin_notices', array( $this, 'review_admin_notices') );
31 + add_action( 'admin_notices', array( $this, 'archive_admin_notices' ) );
27 32 add_action( 'admin_menu', array( $this, 'admin_dashboard_pages' ) );
28 33 add_action( 'admin_head', array( $this, 'admin_head' ) );
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
@@ -29,15 +34,515 @@
29 34 add_action( 'admin_init', array( $this, 'admin_redirects' ) );
30 35 add_action( 'admin_init', array( $this, 'prevent_access_to_admin' ) );
31 36 add_action( 'admin_init', array( $this, 'view_email' ) );
32 37 add_action( 'admin_init', array( $this, 'preview_emails' ) );
38 + add_action( 'admin_init', array( $this, 'record_recently_viewed' ) );
39 + add_action( 'admin_init', array( $this, 'export_applicant_list' ) );
40 + add_action( 'admin_init', array( $this, 'export_sub_grid' ) );
41 + add_action( 'admin_init', array( $this, 'check_hide_demo_data_tab' ) );
42 + add_action( 'admin_init', array( $this, 'check_install_add_on' ) );
43 + add_filter( 'propertyhive_screen_ids', array( $this, 'crm_only_mode_screen_id' ) );
33 44 }
45 +
46 + public function archive_admin_notices()
47 + {
48 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
49 + if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts']))
50 + {
51 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
52 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
53 + if ( $post_type )
54 + {
55 + $post_type_object = get_post_type_object($post_type);
56 + if ( ! $post_type_object ) {
57 + return;
58 + }
59 +
60 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
61 + $count = is_string($_GET['bulk_archived_posts']) ? absint($_GET['bulk_archived_posts']) : 0;
62 +
63 + if ( $post_type_object )
64 + {
65 + $message = sprintf(
66 + /* translators: 1: number of items, 2: post type label */
67 + _n(
68 + '%1$s %2$s moved to archive.',
69 + '%1$s %2$s moved to archive.',
70 + $count,
71 + 'propertyhive'
72 + ),
73 + number_format_i18n( $count ),
74 + $count === 1
75 + ? $post_type_object->labels->singular_name
76 + : $post_type_object->labels->name
77 + );
78 +
79 + printf(
80 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
81 + esc_html( $message )
82 + );
83 + }
84 + }
85 + }
86 +
87 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
88 + if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) )
89 + {
90 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
91 + $post_type = ( isset($_GET['post_type']) && is_string($_GET['post_type']) ) ? sanitize_key( wp_unslash($_GET['post_type']) ) : '';
92 + if ( $post_type )
93 + {
94 + $post_type_object = get_post_type_object($post_type);
95 + if ( ! $post_type_object ) {
96 + return;
97 + }
98 +
99 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
100 + $count = is_string($_GET['bulk_unarchived_posts']) ? absint($_GET['bulk_unarchived_posts']) : 0;
101 +
102 + if ( $post_type_object )
103 + {
104 + $message = sprintf(
105 + /* translators: 1: number of items, 2: post type label */
106 + _n(
107 + '%1$s %2$s removed from archive.',
108 + '%1$s %2$s removed from archive.',
109 + $count,
110 + 'propertyhive'
111 + ),
112 + number_format_i18n( $count ),
113 + $count === 1
114 + ? $post_type_object->labels->singular_name
115 + : $post_type_object->labels->name
116 + );
117 +
118 + printf(
119 + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>',
120 + esc_html( $message )
121 + );
122 + }
123 + }
124 + }
125 + }
126 +
127 + public function crm_only_mode_screen_id( $screen_ids )
128 + {
129 + $current_user = wp_get_current_user();
130 +
131 + $user_id = $current_user->ID;
132 +
133 + $crm_only_mode = get_user_meta( $user_id, 'crm_only_mode', TRUE );
134 +
135 + if ( $crm_only_mode == '1' )
136 + {
137 + $screen_ids[] = 'toplevel_page_ph-settings';
138 + }
139 +
140 + return $screen_ids;
141 + }
142 +
143 + public function check_install_add_on()
144 + {
145 + $request_get = wp_unslash( $_GET );
146 + $ph_action = isset( $request_get['ph_action'] ) && is_string( $request_get['ph_action'] ) ? sanitize_key( $request_get['ph_action'] ) : '';
147 + $encoded_slug = isset( $request_get['ph_add_on_slug'] ) && is_string( $request_get['ph_add_on_slug'] ) ? sanitize_text_field( $request_get['ph_add_on_slug'] ) : '';
148 + $encoded_plugin = isset( $request_get['ph_add_on_plugin'] ) && is_string( $request_get['ph_add_on_plugin'] ) ? sanitize_text_field( $request_get['ph_add_on_plugin'] ) : '';
149 +
150 + if ( 'install_add_on' === $ph_action && '' !== $encoded_slug && '' !== $encoded_plugin )
151 + {
152 + if ( ! current_user_can( 'manage_propertyhive' ) || ! current_user_can( 'install_plugins' ) ) {
153 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
154 + }
155 + check_admin_referer( 'propertyhive-install-add-on' );
156 +
157 + $installed_plugins = get_option( 'propertyhive_pre_pro_add_ons', array());
158 +
159 + if ( empty($installed_plugins) )
160 + {
161 + $installed_plugins = array();
162 + }
163 +
164 + $decoded_slug = base64_decode( $encoded_slug, true );
165 + $decoded_plugin = base64_decode( $encoded_plugin, true );
166 + if ( false === $decoded_slug || false === $decoded_plugin ) {
167 + wp_die( esc_html__( 'Invalid add-on request.', 'propertyhive' ), '', array( 'response' => 400 ) );
168 + }
169 +
170 + $installed_plugins[] = array(
171 + 'slug' => ph_clean( $decoded_slug ),
172 + 'plugin' => ph_clean( $decoded_plugin )
173 + );
174 +
175 + update_option( 'propertyhive_pre_pro_add_ons', $installed_plugins );
176 +
177 + wp_safe_redirect( admin_url('admin.php?page=ph-settings&tab=features') );
178 + die();
179 + }
180 + }
181 +
182 + public function check_hide_demo_data_tab()
183 + {
184 + $request_get = wp_unslash( $_GET );
185 + $tab = isset( $request_get['tab'] ) && is_string( $request_get['tab'] ) ? sanitize_key( $request_get['tab'] ) : '';
186 + $hide_tab = isset( $request_get['hidetab'] ) && is_scalar( $request_get['hidetab'] ) ? (string) $request_get['hidetab'] : '';
187 +
188 + if ( 'demo_data' === $tab && '' !== $hide_tab )
189 + {
190 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
191 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
192 + }
193 + check_admin_referer( 'propertyhive-hide-demo-data' );
194 +
195 + update_option( 'propertyhive_hide_demo_data_tab', 'yes' );
196 + wp_safe_redirect( admin_url('admin.php?page=ph-settings') );
197 + die();
198 + }
199 + }
200 +
201 + public function export_sub_grid()
202 + {
203 + $request_get = wp_unslash( $_GET );
204 + $sub_grid = isset( $request_get['sub_grid'] ) && is_string( $request_get['sub_grid'] ) ? sanitize_key( $request_get['sub_grid'] ) : '';
205 + $raw_record_ids = isset( $request_get['record_ids'] ) && is_string( $request_get['record_ids'] ) ? sanitize_text_field( $request_get['record_ids'] ) : '';
206 +
207 + if ( '' !== $sub_grid )
208 + {
209 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
210 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
211 + }
212 + check_admin_referer( 'propertyhive-export-sub-grid', 'ph_export_nonce' );
213 +
214 + $export_types = array(
215 + 'property-viewings-grid' => 'viewing',
216 + 'contact-viewings-grid' => 'viewing',
217 + 'property-offers-grid' => 'offer',
218 + 'contact-offers-grid' => 'offer',
219 + 'property-sales-grid' => 'sale',
220 + 'contact-sales-grid' => 'sale',
221 + );
222 + $record_ids = '' !== $raw_record_ids
223 + ? array_values( array_filter( array_map( 'absint', explode( '|', $raw_record_ids ) ) ) )
224 + : array();
225 +
226 + if ( ! isset( $export_types[ $sub_grid ] ) || empty( $record_ids ) ) {
227 + wp_die( esc_html__( 'Invalid export request', 'propertyhive' ), '', array( 'response' => 400 ) );
228 + }
229 + foreach ( $record_ids as $record_id ) {
230 + if ( get_post_type( $record_id ) !== $export_types[ $sub_grid ] || ! current_user_can( 'edit_post', $record_id ) ) {
231 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
232 + }
233 + }
234 +
235 + ob_start();
236 +
237 + $df = fopen("php://output", 'w');
238 +
239 + $columns = array( 'id' => __( 'ID', 'propertyhive' ) );
240 +
241 + if ( strpos( $sub_grid, 'viewings' ) !== false )
242 + {
243 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
244 + $columns['property'] = __( 'Property', 'propertyhive' );
245 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
246 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
247 + $columns['negotiator'] = __( 'Attending Negotiator(s)', 'propertyhive' );
248 + $columns['status'] = __( 'Status', 'propertyhive' );
249 + $columns['feedback'] = __( 'Feedback', 'propertyhive' );
250 + }
251 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
252 + {
253 + $columns['datetime'] = __( 'Date/Time', 'propertyhive' );
254 + $columns['property'] = __( 'Property', 'propertyhive' );
255 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
256 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
257 + $columns['status'] = __( 'Status', 'propertyhive' );
258 + $columns['amount'] = __( 'Offer Amount', 'propertyhive' );
259 + }
260 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
261 + {
262 + $columns['date'] = __( 'Date', 'propertyhive' );
263 + $columns['property'] = __( 'Property', 'propertyhive' );
264 + //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' );
265 + $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' );
266 + $columns['status'] = __( 'Status', 'propertyhive' );
267 + $columns['amount'] = __( 'Sale Amount', 'propertyhive' );
268 + }
269 +
270 + fputcsv($df, $columns);
271 +
272 + if ( ! empty( $record_ids ) )
273 + {
274 + if ( !empty($record_ids) )
275 + {
276 + if ( strpos( $sub_grid, 'viewings' ) !== false )
277 + {
278 + $args = array(
279 + 'post_type' => 'viewing',
280 + 'nopaging' => TRUE,
281 + 'fields' => 'ids',
282 + 'post__in' => $record_ids,
283 + 'order' => 'ASC',
284 + 'orderby' => 'meta_value',
285 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked viewing list by its fixed date-time metadata key.
286 + 'meta_key' => '_start_date_time',
287 + );
288 +
289 + $records_query = new WP_Query( $args );
290 +
291 + if ( $records_query->have_posts() )
292 + {
293 + while ( $records_query->have_posts() )
294 + {
295 + $records_query->the_post();
296 +
297 + $viewing = new PH_Viewing( get_the_ID() );
298 +
299 + $property_id = (int)$viewing->_property_id;
300 + $property_address = '';
301 + if ( !empty($property_id) )
302 + {
303 + $property = new PH_Property( $property_id );
304 + $property_address = $property->get_formatted_full_address();
305 + }
306 +
307 + $columns = array(
308 + get_the_ID(),
309 + gmdate("H:i jS F Y", strtotime($viewing->_start_date_time)),
310 + $property_address,
311 + str_replace("<br>", "\n", $viewing->get_applicants()),
312 + $viewing->get_negotiators(),
313 + str_replace("<br>", "\n", $viewing->get_status()),
314 + $viewing->_feedback
315 + );
316 +
317 + fputcsv($df, $columns);
318 + }
319 + }
320 + }
321 + elseif ( strpos( $sub_grid, 'offers' ) !== false )
322 + {
323 + $args = array(
324 + 'post_type' => 'offer',
325 + 'nopaging' => TRUE,
326 + 'fields' => 'ids',
327 + 'post__in' => $record_ids,
328 + 'order' => 'ASC',
329 + 'orderby' => 'meta_value',
330 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked offer list by its fixed date-time metadata key.
331 + 'meta_key' => '_offer_date_time',
332 + );
333 +
334 + $records_query = new WP_Query( $args );
335 +
336 + if ( $records_query->have_posts() )
337 + {
338 + while ( $records_query->have_posts() )
339 + {
340 + $records_query->the_post();
341 +
342 + $offer = new PH_Offer( get_the_ID() );
343 +
344 + $property_id = (int)$offer->_property_id;
345 + $property_address = '';
346 + if ( !empty($property_id) )
347 + {
348 + $property = new PH_Property( $property_id );
349 + $property_address = $property->get_formatted_full_address();
350 + }
351 +
352 + $columns = array(
353 + get_the_ID(),
354 + gmdate("H:i jS F Y", strtotime($offer->_offer_date_time)),
355 + $property_address,
356 + str_replace("<br>", "\n", $offer->get_applicants()),
357 + $offer->_status,
358 + html_entity_decode($offer->get_formatted_amount())
359 + );
360 +
361 + fputcsv($df, $columns);
362 + }
363 + }
364 + }
365 + elseif ( strpos( $sub_grid, 'sales' ) !== false )
366 + {
367 + $args = array(
368 + 'post_type' => 'sale',
369 + 'nopaging' => TRUE,
370 + 'fields' => 'ids',
371 + 'post__in' => $record_ids,
372 + 'order' => 'ASC',
373 + 'orderby' => 'meta_value',
374 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- The export sorts a bounded, capability-checked sale list by its fixed date-time metadata key.
375 + 'meta_key' => '_sale_date_time',
376 + );
377 +
378 + $records_query = new WP_Query( $args );
379 +
380 + if ( $records_query->have_posts() )
381 + {
382 + while ( $records_query->have_posts() )
383 + {
384 + $records_query->the_post();
385 +
386 + $sale = new PH_Sale( get_the_ID() );
387 +
388 + $property_id = (int)$sale->_property_id;
389 + $property_address = '';
390 + if ( !empty($property_id) )
391 + {
392 + $property = new PH_Property( $property_id );
393 + $property_address = $property->get_formatted_full_address();
394 + }
395 +
396 + $columns = array(
397 + get_the_ID(),
398 + gmdate("jS F Y", strtotime($sale->_sale_date_time)),
399 + $property_address,
400 + str_replace("<br>", "\n", $sale->get_applicants()),
401 + $sale->_status,
402 + html_entity_decode($sale->get_formatted_amount())
403 + );
404 +
405 + fputcsv($df, $columns);
406 + }
407 + }
408 + }
409 + }
410 + }
411 +
412 + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream.
413 +
414 + $output = ob_get_clean();
415 +
416 + $filename = sanitize_title( $sub_grid ) . '-' . gmdate("YmdHis") . '.csv';
417 +
418 + // disable caching
419 + $now = gmdate("D, d M Y H:i:s");
420 + header("Expires: Tue, 03 Jul 2001 06:00:00 GMT");
421 + header("Cache-Control: max-age=0, no-cache, must-revalidate, proxy-revalidate");
422 + header("Last-Modified: {$now} GMT");
423 +
424 + // force download
425 + header("Content-Type: application/force-download");
426 + header("Content-Type: application/octet-stream");
427 + header("Content-Type: application/download");
428 +
429 + // disposition / encoding on response body
430 + header("Content-Disposition: attachment;filename={$filename}");
431 + header("Content-Transfer-Encoding: binary");
432 +
433 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download produced by fputcsv, not HTML; HTML escaping would corrupt exported field values.
434 + echo $output;
435 +
436 + die();
437 + }
438 + }
439 +
440 + public function export_applicant_list()
441 + {
442 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
443 + $request_post = wp_unslash( $_POST );
444 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
445 + $submitted_applicant_list = isset( $request_post['submitted_applicant_list'] ) && '1' === (string) $request_post['submitted_applicant_list'];
446 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- These flags only trigger PH_Admin_Applicant_List::export(), which verifies ph_applicant_export_nonce and manage_propertyhive before generating the CSV.
447 + $export_applicant_list_results = isset( $request_post['export_applicant_list_results'] ) && '1' === (string) $request_post['export_applicant_list_results'];
448 +
449 + if ( $submitted_applicant_list && $export_applicant_list_results )
450 + {
451 + include_once( 'class-ph-admin-applicant-list.php' );
452 + $ph_admin_applicant_list = new PH_Admin_Applicant_List();
453 + $ph_admin_applicant_list->export();
454 + }
455 + }
456 +
457 + public function record_recently_viewed()
458 + {
459 + global $pagenow;
460 +
461 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
462 + $request_get = wp_unslash( $_GET );
463 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This records the current user's own read-only navigation history; it performs no cross-user or CRM state change.
464 + $recent_post_id = isset( $request_get['post'] ) && is_scalar( $request_get['post'] ) ? absint( $request_get['post'] ) : 0;
465 +
466 + if (
467 + 'post.php' === $pagenow &&
468 + $recent_post_id > 0 &&
469 + in_array(
470 + get_post_type( $recent_post_id ),
471 + apply_filters( 'propertyhive_post_types_with_tabs', array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale') )
472 + )
473 + )
474 + {
475 + $recently_viewed = get_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', TRUE );
476 +
477 + if ( !is_array($recently_viewed) )
478 + {
479 + $recently_viewed = array();
480 + }
481 +
482 + foreach ( $recently_viewed as $time => $post )
483 + {
484 + if ( $recent_post_id == $post['id'] )
485 + {
486 + unset($recently_viewed[$time]);
487 + }
488 + }
489 +
490 + $title = get_the_title( $recent_post_id );
491 +
492 + switch ( get_post_type( $recent_post_id ) )
493 + {
494 + case "appraisal":
495 + {
496 + $appraisal = new PH_Appraisal( $recent_post_id );
497 + $title = $appraisal->get_formatted_summary_address();
498 + break;
499 + }
500 + case "property":
501 + {
502 + $property = new PH_Property( $recent_post_id );
503 + $title = $property->get_formatted_summary_address();
504 + break;
505 + }
506 + case "enquiry":
507 + case "viewing":
508 + case "offer":
509 + case "sale":
510 + {
511 + $property_id = get_post_meta( $recent_post_id, '_property_id', TRUE );
512 + if ( $property_id != '' )
513 + {
514 + $property = new PH_Property( (int)$property_id );
515 + $title = $property->get_formatted_summary_address();
516 + }
517 + break;
518 + }
519 + }
520 +
521 + $title = ucfirst( get_post_type( $recent_post_id ) ) . ' - ' . $title;
522 +
523 + $recently_viewed = array(time() => array(
524 + 'id' => $recent_post_id,
525 + 'title' => $title,
526 + 'post_type' => get_post_type( $recent_post_id ),
527 + 'edit_link' => get_edit_post_link( $recent_post_id ),
528 + )) + $recently_viewed;
529 +
530 + $recently_viewed = array_slice($recently_viewed, 0, 10, TRUE);
531 +
532 + update_user_meta( get_current_user_id(), '_propertyhive_recently_viewed', $recently_viewed );
533 + }
534 + }
34 535
35 536 public function admin_dashboard_pages()
36 537 {
37 - if ( ! empty( $_GET['page'] ) )
538 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This registers a read-only admin dashboard page and does not change state.
539 + $request_get = wp_unslash( $_GET );
540 + $admin_page = isset( $request_get['page'] ) && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
541 +
542 + if ( '' !== $admin_page )
38 543 {
39 - switch ( sanitize_title($_GET['page']) )
544 + switch ( $admin_page )
40 545 {
41 546 case 'ph-installed':
42 547 {
43 548 add_dashboard_page(
@@ -43,9 +548,9 @@
43 548 add_dashboard_page(
44 549 __( 'Welcome to Property Hive', 'propertyhive' ),
45 550 __( 'Welcome to Property Hive', 'propertyhive' ),
46 551 'manage_propertyhive',
47 - sanitize_title($_GET['page']),
552 + $admin_page,
48 553 array( $this, 'installed_screen' )
49 554 );
50 555
51 556 break;
@@ -58,9 +563,9 @@
58 563 {
59 564 ?>
60 565 <div class="wrap propertyhive-installed-screen">
61 566
62 - <h1><?php _e( 'Welcome to Property Hive', 'propertyhive' ); ?></h1>
567 + <h1><?php echo esc_html(__( 'Welcome to Property Hive', 'propertyhive' )); ?></h1>
63 568
64 569 <div class="intro-text">
65 570 <p>Thank you choosing Property Hive to power your next property website. Below you'll find useful links, tips on getting started, and more.</p>
66 571 </div>
@@ -72,13 +577,13 @@
72 577 <h2>Getting Started</h2>
73 578
74 579 <p>Now that you've installed Property Hive you'll notice a new 'Property Hive' item in the left hand menu of WordPress.</p>
75 580
76 - <img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
581 + <img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/wordpress-menu.png" style="margin:0 auto; display:block; max-width:100%;" alt="Property Hive menu in WordPress">
77 582
78 - <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo admin_url( 'admin.php?page=ph-settings' ); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
583 + <p><strong>Configure Property Hive:</strong> We recommend that you start by navigating to the '<a href="<?php echo esc_url(admin_url( 'admin.php?page=ph-settings' )); ?>" target="_blank">Settings</a>' area of Property Hive and configuring the options available.</p>
79 584
80 - <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo admin_url( 'post-new.php?post_type=property' ); ?>" target="_blank">adding your first property</a>.</p>
585 + <p><strong>Add Your First Property:</strong> See for yourself how easy it is to use Property Hive by <a href="<?php echo esc_url(admin_url( 'post-new.php?post_type=property' )); ?>" target="_blank">adding your first property</a>.</p>
81 586
82 587 </div>
83 588
84 589 <div class="panel">
@@ -86,9 +591,9 @@
86 591 <h2>Extending Property Hive</h2>
87 592
88 593 <p>We have a <a href="https://wp-property-hive.com/add-ons/" target="_blank">wide range of add ons</a> available to add extra functionality to your website.</p>
89 594
90 - <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
595 + <a href="https://wp-property-hive.com/add-ons/" target="_blank"><img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/add-ons.png" style="margin:0 auto; border:1px solid #CCC; display:block; max-width:100%;" alt="Property Hive Free Add Ons"></a>
91 596
92 597 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=free" target="_blank">Free Add Ons</a></strong><br>
93 598 From our template assistant add on to a variety of calculators, these free add ons are great additions to any property website.</p>
94 599
@@ -97,10 +602,10 @@
97 602
98 603 <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=tools" target="_blank">Internal Tools</a></strong><br>
99 604 Add ons aimed to make your life easier and to save you time. Includes Digital Window Displays, Address Lookup and more.</p>
100 605
101 - <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import-export" target="_blank">Import and Export</a></strong><br>
102 - Send your properties to portals like Rightmove, Zoopla and more or import properties from thid party software. These add ons automate the import and export of property data.</p>
606 + <p><strong style="font-size:14px;"><a href="https://wp-property-hive.com/add-ons/?category=import" target="_blank">Import and Export</a></strong><br>
607 + Import properties from third party software or send your properties to portals like Rightmove, Zoopla and more. These add ons automate the import and export of property data.</p>
103 608
104 609 </div>
105 610
106 611 <div class="panel">
@@ -109,13 +614,10 @@
109 614
110 615 We pride ourselves on great support at Property Hive and will always do what we can to help you make create the best site possible. Please find below some useful links relating to our support:
111 616
112 617 <p><strong style="font-size:14px;">Documentation</strong><br>
113 - We have documentation <a href="https://wp-property-hive.com/documentation/" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
618 + We have documentation <a href="https://docs.wp-property-hive.com" target="_blank">available on our website</a> covering setup advice, help with theming, and more.</p>
114 619
115 - <p><strong style="font-size:14px;">Priority One-To-One Support</strong><br>
116 - If you require help quickly, or wish to discuss a bespoke requirement, then <a href="https://wp-property-hive.com/product/12-month-license-key/" target="_blank">priority support</a> might be best for you. With a license key priced at just £49.99 per year you'll not only get priority support but also updates to any add ons you've purchased.</p>
117 -
118 620 <p><strong style="font-size:14px;">Our Support Policy</strong><br>
119 621 Our <a href="https://wp-property-hive.com/support-policy/" target="_blank">Support Policy is available to view here</a> and outlines how you can get in touch, how we will (and won't) help, and how to report bugs.</p>
120 622
121 623 </div>
@@ -126,12 +628,12 @@
126 628
127 629 <p><strong style="font-size:14px;">Need a Theme?</strong><br>
128 630 Property Hive does <a href="https://wp-property-hive.com/which-wordpress-themes-work-with-property-hive/" target="_blank">integrate with any new or existing theme</a>. If however you need to get up and running quickly, or just want to have a play before committing, then our free <a href="https://wp-property-hive.com/honeycomb" target="_blank">Honeycomb theme</a> might be right for you.</p>
129 631
130 - <a href="https://wp-property-hive.com/honeycomb" target="_blank"><img src="<?php echo PH()->plugin_url(); ?>/assets/images/admin/installed-screen/honeycomb-screenshot.png" style="margin:0 auto; display:block; max-width:80%;" alt="Property Hive Free Honeycomb Theme"></a>
632 + <a href="https://wp-property-hive.com/honeycomb" target="_blank"><img src="<?php echo esc_url(PH()->plugin_url()); ?>/assets/images/admin/installed-screen/honeycomb-screenshot.png" style="margin:0 auto; display:block; max-width:80%;" alt="Property Hive Free Honeycomb Theme"></a>
131 633
132 634 <p><strong style="font-size:14px;">Leave a Review</strong><br>
133 - If you've found Property Hive useful we'd love it if you could spare a moment to tell others just how great we are by <a href="https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5" target="_blank">leaving a review</a>.</p>
635 + If you've found Property Hive useful we'd love it if you could spare a moment to tell others just how great we are by <a href="https://wordpress.org/support/plugin/propertyhive/reviews/" target="_blank">leaving a review</a>.</p>
134 636
135 637 <p><strong style="font-size:14px;">Contribute</strong><br>
136 638 Property Hive is completely open-source meaning anyone can access and contribute to the code. Fixing bugs and adding functionality can be done by anyone with coding knowledge. <a href="https://github.com/propertyhive/WP-Property-Hive" target="_blank">Visit us on GitHub</a> to get started.</p>
137 639
@@ -168,14 +670,21 @@
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( 'class-ph-admin-onboarding.php' );
675 + include_once( dirname(PH_PLUGIN_FILE) . '/includes/class-ph-ai-service.php' );
173 676
174 677 // Classes we only need if the ajax is not-ajax
175 678 if ( ! is_ajax() ) {
176 679 include( 'class-ph-admin-menus.php' );
177 680 include( 'class-ph-admin-assets.php' );
681 +
682 + // Help Tab
683 + if ( apply_filters( 'propertyhive_enable_admin_help_tab', true ) )
684 + {
685 + include_once( 'class-ph-admin-help.php' );
686 + }
178 687 }
179 688 }
180 689
181 690 /**
@@ -192,8 +701,14 @@
192 701 break;
193 702 case 'plugins' :
194 703 include( 'class-ph-admin-plugin-updates.php' );
195 704 break;
705 + case 'users':
706 + case 'user':
707 + case 'profile':
708 + case 'user-edit':
709 + include( 'class-ph-admin-profile.php' );
710 + break;
196 711 }
197 712 }
198 713
199 714 /**
@@ -225,8 +740,19 @@
225 740 }
226 741
227 742 public function review_admin_notices()
228 743 {
744 + global $wpdb;
745 +
746 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This method only renders read-only admin notices.
747 + $request_get = wp_unslash( $_GET );
748 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- This method only checks whether a settings POST is present to suppress a duplicate read-only notice; it does not process or save the value.
749 + $request_post = wp_unslash( $_POST );
750 + $admin_page_present = isset( $request_get['page'] );
751 + $admin_page = $admin_page_present && is_string( $request_get['page'] ) ? sanitize_title( $request_get['page'] ) : '';
752 + $plugin_status_present = isset( $request_get['plugin_status'] );
753 + $maps_api_key_submitted = isset( $request_post['propertyhive_google_maps_api_key'] );
754 +
229 755 if ( current_user_can( 'manage_options' ) )
230 756 {
231 757 $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 );
232 758 if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 )
@@ -234,12 +760,12 @@
234 760 if ( $propertyhive_review_prompt_due_timestamp < time() )
235 761 {
236 762 echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\">
237 763 <p>
238 - " . __( '<strong>Finding Property Hive useful?</strong> Please take a minute to <a href="https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post" target="_blank">leave us a ★★★★★ review</a>', 'propertyhive' ) . "
764 + " . wp_kses_post( __( '<strong>Finding Property Hive useful?</strong> Please take a minute to <a href="https://wordpress.org/support/plugin/propertyhive/reviews/#new-post" target="_blank">leave us a review</a>', 'propertyhive' ) ) . "
239 765 </p>
240 766 <p>
241 - <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/?filter=5#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
767 + <a href=\"https://wordpress.org/support/plugin/propertyhive/reviews/#new-post\" target=\"_blank\" class=\"button-primary\">Leave a Review</a>
242 768 <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a>
243 769 </p>
244 770 </div>";
245 771 }
@@ -245,14 +771,57 @@
245 771 }
246 772 }
247 773
248 774 if (
775 + class_exists('Easy_Property_Listings') &&
776 + ! $plugin_status_present &&
777 + get_option( 'epl_notice_dismissed', '' ) != 'yes'
778 + )
779 + {
780 + echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\">
781 + <p>
782 + " . wp_kses_post( __( '<strong>It looks like you\'re also running Easy Property Listings.</strong> This will cause conflicts with Property Hive and should be deactivated.', 'propertyhive' ) ) . "
783 + </p>
784 + <p>
785 + <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a>
786 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a>
787 + </p>
788 +
789 + </div>";
790 + }
791 +
792 + if (
793 + !class_exists('PH_Demo_Data') &&
794 + get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 &&
795 + get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' &&
796 + (
797 + ! $admin_page_present
798 + ||
799 + (
800 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
801 + )
802 + )
803 + )
804 + {
805 + echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\">
806 + <p>
807 + " . wp_kses_post( __( '<strong>New To Property Hive?</strong> Did you know that you can quickly import demo data to get a feel for how Property Hive works?', 'propertyhive' ) ) . "
808 + </p>
809 + <p>
810 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a>
811 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a>
812 + </p>
813 +
814 + </div>";
815 + }
816 +
817 + if (
249 818 get_option('propertyhive_search_results_page_id', '') == '' &&
250 819 (
251 - !isset($_GET['page'])
820 + ! $admin_page_present
252 821 ||
253 822 (
254 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
823 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
255 824 )
256 825 ) &&
257 826 get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes'
258 827 )
@@ -258,13 +827,13 @@
258 827 )
259 828 {
260 829 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\">
261 830 <p>
262 - " . __( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
831 + " . esc_html__( 'We noticed that you haven\'t assigned a page to be your \'Search Results\' page yet. We recommend that you do this in order to display properties on your site.', 'propertyhive' ) . "
263 832 </p>
264 833 <p>
265 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general') . "\" class=\"button-primary\">Go To Property Hive Settings</a>
266 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">Dismiss</a>
834 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To Property Hive Settings', 'propertyhive' )) . "</a>
835 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
267 836 </p>
268 837
269 838 </div>";
270 839 }
@@ -269,15 +838,17 @@
269 838 </div>";
270 839 }
271 840
272 841 if (
842 + get_option('propertyhive_maps_provider') !== 'osm' &&
843 + get_option('propertyhive_maps_provider') !== 'mapbox' &&
273 844 get_option('propertyhive_google_maps_api_key', '') == '' &&
274 - !isset($_POST['propertyhive_google_maps_api_key']) &&
845 + ! $maps_api_key_submitted &&
275 846 (
276 - !isset($_GET['page'])
847 + ! $admin_page_present
277 848 ||
278 849 (
279 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed'
850 + $admin_page_present && 'ph-installed' !== $admin_page
280 851 )
281 852 ) &&
282 853 get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes'
283 854 )
@@ -283,13 +854,17 @@
283 854 )
284 855 {
285 856 echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\">
286 857 <p>
287 - " . __( 'We noticed that you haven\'t entered a Google Maps API key yet. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="'. admin_url('admin.php?page=ph-settings&tab=general&section=map') . '">enter it</a>.', 'propertyhive' ) . "
858 + " . sprintf(
859 + /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */
860 + wp_kses_post( __( 'We noticed that you haven\'t entered a Google Maps API key. If wishing to display a map on your website it\'s recommended that you <a href="https://developers.google.com/maps/documentation/javascript/get-api-key" target="_blank">create one</a> and <a href="%s">enter it</a>.', 'propertyhive' ) ),
861 + esc_url( admin_url('admin.php?page=ph-settings&tab=general&section=map') )
862 + ) . "
288 863 </p>
289 864 <p>
290 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=general&section=map') . "\" class=\"button-primary\">Enter Google Maps API Key</a>
291 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">Dismiss</a>
865 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general&section=map')) . "\" class=\"button-primary\">" . esc_html(__( 'Enter Google Maps API Key', 'propertyhive' )) . "</a>
866 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_google_maps_api_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
292 867 </p>
293 868
294 869 </div>";
295 870 }
@@ -297,12 +872,12 @@
297 872 if (
298 873 get_option('propertyhive_license_key', '') != '' &&
299 874 get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' &&
300 875 (
301 - !isset($_GET['page'])
876 + ! $admin_page_present
302 877 ||
303 878 (
304 - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings'
879 + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page
305 880 )
306 881 )
307 882 )
308 883 {
@@ -310,32 +885,13 @@
310 885 $output = '';
311 886
312 887 if ( isset($license['active']) && $license['active'] != '1' )
313 888 {
314 - $output = __( 'You\'re Property Hive license key is inactive.', 'propertyhive' );
889 + $output = __( 'Your Property Hive license key is inactive.', 'propertyhive' );
315 890 }
316 891 else
317 892 {
318 - if ( isset($license['expires_at']) && $license['expires_at'] != '' )
319 - {
320 - if ( strtotime($license['expires_at']) <= time() )
321 - {
322 - // Expired
323 - $output = __( 'Your Property Hive license key expired on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
324 - }
325 - elseif (
326 - strtotime($license['expires_at']) > time() &&
327 - strtotime($license['expires_at']) < (time() + 30 * 24 * 60 * 60)
328 - )
329 - {
330 - // Expires in less than 30 days
331 - $output = __( 'Your Property Hive license key expires on ' . date("jS F Y", strtotime($license['expires_at'])), 'propertyhive' ) . '. It\'s recommended that you renew it to ensure you continue to receive future updates to add ons you\'ve purchased.';
332 - }
333 - elseif (strtotime($license['expires_at']) > time())
334 - {
335 - // Valid
336 - }
337 - }
893 +
338 894 }
339 895
340 896 if ( $output != '' )
341 897 {
@@ -340,19 +896,49 @@
340 896 if ( $output != '' )
341 897 {
342 898 echo "<div class=\"notice notice-info\" id=\"ph_notice_invalid_expired_license_key\">
343 899 <p>
344 - " . $output . "
900 + " . esc_html($output) . "
345 901 </p>
346 902 <p>
347 - <a href=\"". admin_url('admin.php?page=ph-settings&tab=licensekey') . "\" class=\"button-primary\">Go To License Key Settings</a>
348 - <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">Dismiss</a>
903 + <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=licensekey')) . "\" class=\"button-primary\">" . esc_html(__( 'Go To License Key Settings', 'propertyhive' )) . "</a>
904 + <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_invalid_expired_license_key\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a>
349 905 </p>
350 906
351 907 </div>";
352 908 }
353 909 }
910 +
911 + $screen = get_current_screen();
912 + if ( in_array( $screen->id, array( 'dashboard' ) ) )
913 + {
914 + // Email Cron Warning
915 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- The email queue is a custom plugin table; this read-only dashboard notice has no WordPress API equivalent.
916 + $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1");
917 + $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log');
918 + if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) )
919 + {
920 + echo '
921 + <div class="notice notice-error" id="ph_notice_email_cron_not_running">
922 + <p>' . esc_html(__( 'The Property Hive email queue does not appear to be running', 'propertyhive' )) . '
923 + </p>
924 + <p>
925 + <a href="'. esc_url(admin_url('admin.php?page=ph-settings&tab=email&section=log&status=queued')) . '" class="button-primary">' . esc_html(__( 'Go To Email Queue', 'propertyhive' )) . '</a>
926 + </p>
927 + </div>
928 + ';
929 + }
930 + }
354 931 }
932 +
933 + if ( isset( $request_get['propertyhive_contacts_merged'] ) )
934 + {
935 + echo '
936 + <div class="notice notice-info">
937 + <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p>
938 + </div>
939 + ';
940 + }
355 941 }
356 942
357 943 /**
358 944 * Handle redirects to welcome page after install.
@@ -364,13 +950,14 @@
364 950 {
365 951 delete_transient( '_ph_activation_redirect' );
366 952
367 953 // Don't do redirect if part of multisite, doing batch-activate, or if no permission
368 - if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) {
954 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change.
955 + if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_options' ) ) {
369 956 return;
370 957 }
371 958
372 - wp_safe_redirect( admin_url( 'index.php?page=ph-installed' ) );
959 + wp_safe_redirect( admin_url( 'index.php?page=ph-onboarding' ) );
373 960 exit;
374 961 }
375 962 }
376 963
@@ -383,9 +970,10 @@
383 970
384 971 // Check role, but also AJAX as request to admin-ajax.php will still need to be made
385 972 if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' )
386 973 {
387 - exit( wp_redirect( home_url( '/' ) ) );
974 + wp_safe_redirect( home_url( '/' ) );
975 + exit;
388 976 }
389 977 }
390 978
391 979 /**
@@ -398,19 +986,39 @@
398 986 global $wpdb;
399 987
400 988 if ( isset( $_GET['view_propertyhive_email'] ) )
401 989 {
402 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'view-email' ) )
990 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
991 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
992 + }
993 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'view-email' ) )
403 994 {
404 - die( 'Security check' );
995 + wp_die( 'Security check' );
405 996 }
406 997
998 + if ( ! current_user_can( 'manage_propertyhive' ) )
999 + {
1000 + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) );
1001 + }
1002 +
407 1003 if ( isset( $_GET['email_id'] ) )
408 1004 {
409 - $email_log = $wpdb->get_row( "SELECT * FROM " . $wpdb->prefix . "ph_email_log WHERE email_id = '" . esc_sql( (int)$_GET['email_id'] ) . "'" );
1005 + $email_id = is_string( $_GET['email_id'] ) ? absint( $_GET['email_id'] ) : 0;
1006 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Email logs are stored in a custom plugin table and this is a single protected administrative lookup.
1007 + $email_log = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}ph_email_log WHERE email_id = %d", $email_id ) );
410 1008 if ( null !== $email_log )
411 1009 {
412 - echo apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $email_log->body ) ) );
1010 + $body = $email_log->body;
1011 +
1012 + if ( extension_loaded('zlib') && @gzuncompress($body) !== false )
1013 + {
1014 + $body = gzuncompress($body);
1015 + }
1016 +
1017 + $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
1018 +
1019 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is the rendered HTML email viewer. The body was sanitized before entering the email log; propertyhive_mail_content and email templates are intentional trusted HTML extension points.
1020 + echo $message;
413 1021
414 1022 }
415 1023 else
416 1024 {
@@ -429,28 +1037,64 @@
429 1037 */
430 1038 public function preview_emails() {
431 1039 if ( isset( $_GET['preview_propertyhive_email'] ) )
432 1040 {
433 - if ( ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-properties' ) && ! wp_verify_nonce( $_REQUEST['_wpnonce'], 'propertyhive-matching-applicants' ) )
1041 + if ( ! current_user_can( 'manage_propertyhive' ) ) {
1042 + wp_die( esc_html__( 'Insufficient permissions', 'propertyhive' ), '', array( 'response' => 403 ) );
1043 + }
1044 + if ( ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-properties' ) && ! wp_verify_nonce( ( isset( $_REQUEST['_wpnonce'] ) && is_string( $_REQUEST['_wpnonce'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '', 'propertyhive-matching-applicants' ) )
434 1045 {
435 1046 die( 'Security check' );
436 1047 }
437 1048
1049 + $current_user = wp_get_current_user();
1050 + $request_get = wp_unslash( $_GET );
1051 + $request_post = wp_unslash( $_POST );
1052 +
438 1053 // get the preview email content
439 - if ( isset($_GET['property_id']) )
1054 + $email_property_ids = array();
1055 + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) )
440 1056 {
441 - $email_property_ids = array((int)$_GET['property_id']);
1057 + $email_property_ids = array( absint( $request_get['property_id'] ) );
442 1058 }
443 - elseif ( isset($_POST['email_property_id']) )
1059 + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) )
444 1060 {
445 - $email_property_ids = explode(",", sanitize_text_field($_POST['email_property_id']));
1061 + $email_property_ids = array_values( array_filter( array_map( 'absint', explode( ',', sanitize_text_field( $request_post['email_property_id'] ) ) ) ) );
446 1062 }
447 1063
448 - $body = stripslashes(sanitize_textarea_field($_POST['body']));
1064 + $allowed_tags = array(
1065 + 'strong' => array(),
1066 + 'span' => array(),
1067 + 'em' => array(),
1068 + 'h1' => array(),
1069 + 'h2' => array(),
1070 + 'h3' => array(),
1071 + 'h4' => array(),
1072 + 'h5' => array(),
1073 + 'h6' => array(),
1074 + 'i' => array(),
1075 + 'u' => array(),
1076 + 'b' => array(),
1077 + 'a' => array(
1078 + 'href' => array(),
1079 + 'target' => array(),
1080 + ),
1081 + );
1082 + $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags );
449 1083
450 - $body = str_replace("[contact_name]", get_the_title((int)$_GET['contact_id']), $body);
451 - $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body);
1084 + $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : '';
1085 + $body = wp_kses( $raw_body, $allowed_tags );
452 1086
1087 + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) )
1088 + {
1089 + $contact = new PH_Contact( absint( $request_get['contact_id'] ) );
1090 + $body = str_replace( '[contact_name]', esc_html( $contact->post_title ), $body );
1091 + $body = str_replace( '[contact_dear]', esc_html( $contact->dear() ), $body );
1092 + }
1093 + $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body );
1094 +
1095 + $office_counts = array();
1096 +
453 1097 if ( strpos($body, '[properties]') !== FALSE )
454 1098 {
455 1099 ob_start();
456 1100
@@ -458,8 +1102,15 @@
458 1102 {
459 1103 foreach ( $email_property_ids as $email_property_id )
460 1104 {
461 1105 $property = new PH_Property((int)$email_property_id);
1106 +
1107 + if ( $property->office_id != '' && $property->office_id != 0 )
1108 + {
1109 + if ( !isset($office_counts[$property->office_id]) ) { $office_counts[$property->office_id] = 0; }
1110 + ++$office_counts[$property->office_id];
1111 + }
1112 +
462 1113 ph_get_template( 'emails/applicant-match-property.php', array( 'property' => $property ) );
463 1114 }
464 1115 }
465 1116 $body = str_replace("[properties]", ob_get_clean(), $body);
@@ -464,15 +1115,40 @@
464 1115 }
465 1116 $body = str_replace("[properties]", ob_get_clean(), $body);
466 1117 }
467 1118
468 - // create a new email
469 - $email = new PH_Emails();
1119 + $office_name = '';
1120 + $office_email_address = '';
470 1121
1122 + $office_id = get_user_meta($current_user->ID, 'office_id', TRUE);
1123 + if ($office_id == '')
1124 + {
1125 + // No office against user. Use email address of office with most properties
1126 + if ( !empty($office_counts) )
1127 + {
1128 + arsort($office_counts);
1129 + reset($office_counts);
1130 + $office_id = key($office_counts);
1131 + }
1132 + }
1133 +
1134 + if ( !empty($office_id) )
1135 + {
1136 + $office_name = get_the_title( (int) $office_id );
1137 + $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE );
1138 + }
1139 +
1140 + $body = str_replace( '[office_name]', esc_html( $office_name ), $body );
1141 + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body );
1142 +
1143 + $body = str_replace( '[negotiator_name]', esc_html( $current_user->display_name ), $body );
1144 + $body = str_replace( '[negotiator_email_address]', esc_html( $current_user->user_email ), $body );
1145 +
471 1146 // wrap the content with the email template and then add styles
472 - $message = apply_filters( 'propertyhive_mail_content', $email->style_inline( $email->wrap_message( $body ) ) );
1147 + $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) );
473 1148
474 1149 // print the preview email
1150 + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- This is the rendered HTML email preview. The request body was passed through the explicit match allowlist; templates and propertyhive_mail_content are intentional trusted HTML extension points.
475 1151 echo $message;
476 1152 exit;
477 1153 }
478 1154 }
@@ -477,5 +1153,5 @@
477 1153 }
478 1154 }
479 1155 }
480 1156
481 -return new PH_Admin();
1157 +return new PH_Admin();