| @@ -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 |
| @@ -40,65 +44,83 @@ | ||
| 40 | 44 | } |
| 41 | 45 | |
| 42 | 46 | public function archive_admin_notices() |
| 43 | 47 | { |
| 48 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change. | |
| 44 | 49 | if ( isset($_GET['bulk_archived_posts']) && !empty($_GET['bulk_archived_posts'])) |
| 45 | 50 | { |
| 46 | - $post_type = isset($_GET['post_type']) ? $_GET['post_type'] : ''; | |
| 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']) ) : ''; | |
| 47 | 53 | if ( $post_type ) |
| 48 | 54 | { |
| 49 | 55 | $post_type_object = get_post_type_object($post_type); |
| 56 | + if ( ! $post_type_object ) { | |
| 57 | + return; | |
| 58 | + } | |
| 50 | 59 | |
| 51 | - $count = intval($_GET['bulk_archived_posts']); | |
| 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; | |
| 52 | 62 | |
| 53 | - $message = sprintf( | |
| 54 | - /* translators: 1: number of items, 2: post type label */ | |
| 55 | - _n( | |
| 56 | - '%1$s %2$s moved to archive.', | |
| 57 | - '%1$s %2$s moved to archive.', | |
| 58 | - $count, | |
| 59 | - 'propertyhive' | |
| 60 | - ), | |
| 61 | - number_format_i18n( $count ), | |
| 62 | - $count === 1 | |
| 63 | - ? $post_type_object->labels->singular_name | |
| 64 | - : $post_type_object->labels->name | |
| 65 | - ); | |
| 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 | + ); | |
| 66 | 78 | |
| 67 | - printf( | |
| 68 | - '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>', | |
| 69 | - $message | |
| 70 | - ); | |
| 79 | + printf( | |
| 80 | + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>', | |
| 81 | + esc_html( $message ) | |
| 82 | + ); | |
| 83 | + } | |
| 71 | 84 | } |
| 72 | 85 | } |
| 73 | 86 | |
| 87 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin list display or query; no state change. | |
| 74 | 88 | if ( isset($_GET['bulk_unarchived_posts']) && !empty($_GET['bulk_unarchived_posts']) ) |
| 75 | 89 | { |
| 76 | - $post_type = isset($_GET['post_type']) ? $_GET['post_type'] : ''; | |
| 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']) ) : ''; | |
| 77 | 92 | if ( $post_type ) |
| 78 | 93 | { |
| 79 | 94 | $post_type_object = get_post_type_object($post_type); |
| 95 | + if ( ! $post_type_object ) { | |
| 96 | + return; | |
| 97 | + } | |
| 80 | 98 | |
| 81 | - $count = intval($_GET['bulk_unarchived_posts']); | |
| 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; | |
| 82 | 101 | |
| 83 | - $message = sprintf( | |
| 84 | - /* translators: 1: number of items, 2: post type label */ | |
| 85 | - _n( | |
| 86 | - '%1$s %2$s removed from archive.', | |
| 87 | - '%1$s %2$s removed from archive.', | |
| 88 | - $count, | |
| 89 | - 'propertyhive' | |
| 90 | - ), | |
| 91 | - number_format_i18n( $count ), | |
| 92 | - $count === 1 | |
| 93 | - ? $post_type_object->labels->singular_name | |
| 94 | - : $post_type_object->labels->name | |
| 95 | - ); | |
| 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 | + ); | |
| 96 | 117 | |
| 97 | - printf( | |
| 98 | - '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>', | |
| 99 | - $message | |
| 100 | - ); | |
| 118 | + printf( | |
| 119 | + '<div id="message" class="notice is-dismissible updated"><p>%s</p></div>', | |
| 120 | + esc_html( $message ) | |
| 121 | + ); | |
| 122 | + } | |
| 101 | 123 | } |
| 102 | 124 | } |
| 103 | 125 | } |
| 104 | 126 | |
| @@ -119,14 +141,20 @@ | ||
| 119 | 141 | } |
| 120 | 142 | |
| 121 | 143 | public function check_install_add_on() |
| 122 | 144 | { |
| 123 | - if ( | |
| 124 | - isset($_GET['ph_action']) && $_GET['ph_action'] == 'install_add_on' && | |
| 125 | - isset($_GET['ph_add_on_slug']) && !empty($_GET['ph_add_on_slug']) && | |
| 126 | - isset($_GET['ph_add_on_plugin']) && !empty($_GET['ph_add_on_plugin']) | |
| 127 | - ) | |
| 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 ) | |
| 128 | 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 | + | |
| 129 | 157 | $installed_plugins = get_option( 'propertyhive_pre_pro_add_ons', array()); |
| 130 | 158 | |
| 131 | 159 | if ( empty($installed_plugins) ) |
| 132 | 160 | { |
| @@ -132,16 +160,22 @@ | ||
| 132 | 160 | { |
| 133 | 161 | $installed_plugins = array(); |
| 134 | 162 | } |
| 135 | 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 | + | |
| 136 | 170 | $installed_plugins[] = array( |
| 137 | - 'slug' => ph_clean(base64_decode($_GET['ph_add_on_slug'])), | |
| 138 | - 'plugin' => ph_clean(base64_decode($_GET['ph_add_on_plugin'])) | |
| 171 | + 'slug' => ph_clean( $decoded_slug ), | |
| 172 | + 'plugin' => ph_clean( $decoded_plugin ) | |
| 139 | 173 | ); |
| 140 | 174 | |
| 141 | 175 | update_option( 'propertyhive_pre_pro_add_ons', $installed_plugins ); |
| 142 | 176 | |
| 143 | - wp_redirect( admin_url('admin.php?page=ph-settings&tab=features') ); | |
| 177 | + wp_safe_redirect( admin_url('admin.php?page=ph-settings&tab=features') ); | |
| 144 | 178 | die(); |
| 145 | 179 | } |
| 146 | 180 | } |
| 147 | 181 | |
| @@ -146,12 +180,21 @@ | ||
| 146 | 180 | } |
| 147 | 181 | |
| 148 | 182 | public function check_hide_demo_data_tab() |
| 149 | 183 | { |
| 150 | - if ( isset($_GET['tab']) && $_GET['tab'] == 'demo_data' && isset($_GET['hidetab']) ) | |
| 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 ) | |
| 151 | 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 | + | |
| 152 | 195 | update_option( 'propertyhive_hide_demo_data_tab', 'yes' ); |
| 153 | - wp_redirect( admin_url('admin.php?page=ph-settings') ); | |
| 196 | + wp_safe_redirect( admin_url('admin.php?page=ph-settings') ); | |
| 154 | 197 | die(); |
| 155 | 198 | } |
| 156 | 199 | } |
| 157 | 200 | |
| @@ -156,12 +199,40 @@ | ||
| 156 | 199 | } |
| 157 | 200 | |
| 158 | 201 | public function export_sub_grid() |
| 159 | 202 | { |
| 160 | - if ( | |
| 161 | - isset($_GET['sub_grid']) && !empty(ph_clean($_GET['sub_grid'])) | |
| 162 | - ) | |
| 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 ) | |
| 163 | 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 | + | |
| 164 | 235 | ob_start(); |
| 165 | 236 | |
| 166 | 237 | $df = fopen("php://output", 'w'); |
| 167 | 238 | |
| @@ -166,9 +237,9 @@ | ||
| 166 | 237 | $df = fopen("php://output", 'w'); |
| 167 | 238 | |
| 168 | 239 | $columns = array( 'id' => __( 'ID', 'propertyhive' ) ); |
| 169 | 240 | |
| 170 | - if ( strpos(ph_clean($_GET['sub_grid']), 'viewings') ) | |
| 241 | + if ( strpos( $sub_grid, 'viewings' ) !== false ) | |
| 171 | 242 | { |
| 172 | 243 | $columns['datetime'] = __( 'Date/Time', 'propertyhive' ); |
| 173 | 244 | $columns['property'] = __( 'Property', 'propertyhive' ); |
| 174 | 245 | //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' ); |
| @@ -176,9 +247,9 @@ | ||
| 176 | 247 | $columns['negotiator'] = __( 'Attending Negotiator(s)', 'propertyhive' ); |
| 177 | 248 | $columns['status'] = __( 'Status', 'propertyhive' ); |
| 178 | 249 | $columns['feedback'] = __( 'Feedback', 'propertyhive' ); |
| 179 | 250 | } |
| 180 | - elseif ( strpos(ph_clean($_GET['sub_grid']), 'offers') ) | |
| 251 | + elseif ( strpos( $sub_grid, 'offers' ) !== false ) | |
| 181 | 252 | { |
| 182 | 253 | $columns['datetime'] = __( 'Date/Time', 'propertyhive' ); |
| 183 | 254 | $columns['property'] = __( 'Property', 'propertyhive' ); |
| 184 | 255 | //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' ); |
| @@ -185,9 +256,9 @@ | ||
| 185 | 256 | $columns['applicant'] = __( 'Applicant(s)', 'propertyhive' ); |
| 186 | 257 | $columns['status'] = __( 'Status', 'propertyhive' ); |
| 187 | 258 | $columns['amount'] = __( 'Offer Amount', 'propertyhive' ); |
| 188 | 259 | } |
| 189 | - elseif ( strpos(ph_clean($_GET['sub_grid']), 'sales') ) | |
| 260 | + elseif ( strpos( $sub_grid, 'sales' ) !== false ) | |
| 190 | 261 | { |
| 191 | 262 | $columns['date'] = __( 'Date', 'propertyhive' ); |
| 192 | 263 | $columns['property'] = __( 'Property', 'propertyhive' ); |
| 193 | 264 | //$columns['owner'] = __( 'Owner/Landlord', 'propertyhive' ); |
| @@ -197,15 +268,13 @@ | ||
| 197 | 268 | } |
| 198 | 269 | |
| 199 | 270 | fputcsv($df, $columns); |
| 200 | 271 | |
| 201 | - if ( isset($_GET['record_ids']) && !empty(ph_clean($_GET['record_ids'])) ) | |
| 272 | + if ( ! empty( $record_ids ) ) | |
| 202 | 273 | { |
| 203 | - $record_ids = explode("|", ph_clean($_GET['record_ids'])); | |
| 204 | - | |
| 205 | 274 | if ( !empty($record_ids) ) |
| 206 | 275 | { |
| 207 | - if ( strpos(ph_clean($_GET['sub_grid']), 'viewings') ) | |
| 276 | + if ( strpos( $sub_grid, 'viewings' ) !== false ) | |
| 208 | 277 | { |
| 209 | 278 | $args = array( |
| 210 | 279 | 'post_type' => 'viewing', |
| 211 | 280 | 'nopaging' => TRUE, |
| @@ -212,8 +281,9 @@ | ||
| 212 | 281 | 'fields' => 'ids', |
| 213 | 282 | 'post__in' => $record_ids, |
| 214 | 283 | 'order' => 'ASC', |
| 215 | 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. | |
| 216 | 286 | 'meta_key' => '_start_date_time', |
| 217 | 287 | ); |
| 218 | 288 | |
| 219 | 289 | $records_query = new WP_Query( $args ); |
| @@ -235,9 +305,9 @@ | ||
| 235 | 305 | } |
| 236 | 306 | |
| 237 | 307 | $columns = array( |
| 238 | 308 | get_the_ID(), |
| 239 | - date("H:i jS F Y", strtotime($viewing->_start_date_time)), | |
| 309 | + gmdate("H:i jS F Y", strtotime($viewing->_start_date_time)), | |
| 240 | 310 | $property_address, |
| 241 | 311 | str_replace("<br>", "\n", $viewing->get_applicants()), |
| 242 | 312 | $viewing->get_negotiators(), |
| 243 | 313 | str_replace("<br>", "\n", $viewing->get_status()), |
| @@ -247,9 +317,9 @@ | ||
| 247 | 317 | fputcsv($df, $columns); |
| 248 | 318 | } |
| 249 | 319 | } |
| 250 | 320 | } |
| 251 | - elseif ( strpos(ph_clean($_GET['sub_grid']), 'offers') ) | |
| 321 | + elseif ( strpos( $sub_grid, 'offers' ) !== false ) | |
| 252 | 322 | { |
| 253 | 323 | $args = array( |
| 254 | 324 | 'post_type' => 'offer', |
| 255 | 325 | 'nopaging' => TRUE, |
| @@ -256,8 +326,9 @@ | ||
| 256 | 326 | 'fields' => 'ids', |
| 257 | 327 | 'post__in' => $record_ids, |
| 258 | 328 | 'order' => 'ASC', |
| 259 | 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. | |
| 260 | 331 | 'meta_key' => '_offer_date_time', |
| 261 | 332 | ); |
| 262 | 333 | |
| 263 | 334 | $records_query = new WP_Query( $args ); |
| @@ -279,9 +350,9 @@ | ||
| 279 | 350 | } |
| 280 | 351 | |
| 281 | 352 | $columns = array( |
| 282 | 353 | get_the_ID(), |
| 283 | - date("H:i jS F Y", strtotime($offer->_offer_date_time)), | |
| 354 | + gmdate("H:i jS F Y", strtotime($offer->_offer_date_time)), | |
| 284 | 355 | $property_address, |
| 285 | 356 | str_replace("<br>", "\n", $offer->get_applicants()), |
| 286 | 357 | $offer->_status, |
| 287 | 358 | html_entity_decode($offer->get_formatted_amount()) |
| @@ -290,9 +361,9 @@ | ||
| 290 | 361 | fputcsv($df, $columns); |
| 291 | 362 | } |
| 292 | 363 | } |
| 293 | 364 | } |
| 294 | - elseif ( strpos(ph_clean($_GET['sub_grid']), 'sales') ) | |
| 365 | + elseif ( strpos( $sub_grid, 'sales' ) !== false ) | |
| 295 | 366 | { |
| 296 | 367 | $args = array( |
| 297 | 368 | 'post_type' => 'sale', |
| 298 | 369 | 'nopaging' => TRUE, |
| @@ -299,8 +370,9 @@ | ||
| 299 | 370 | 'fields' => 'ids', |
| 300 | 371 | 'post__in' => $record_ids, |
| 301 | 372 | 'order' => 'ASC', |
| 302 | 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. | |
| 303 | 375 | 'meta_key' => '_sale_date_time', |
| 304 | 376 | ); |
| 305 | 377 | |
| 306 | 378 | $records_query = new WP_Query( $args ); |
| @@ -322,9 +394,9 @@ | ||
| 322 | 394 | } |
| 323 | 395 | |
| 324 | 396 | $columns = array( |
| 325 | 397 | get_the_ID(), |
| 326 | - date("jS F Y", strtotime($sale->_sale_date_time)), | |
| 398 | + gmdate("jS F Y", strtotime($sale->_sale_date_time)), | |
| 327 | 399 | $property_address, |
| 328 | 400 | str_replace("<br>", "\n", $sale->get_applicants()), |
| 329 | 401 | $sale->_status, |
| 330 | 402 | html_entity_decode($sale->get_formatted_amount()) |
| @@ -336,13 +408,13 @@ | ||
| 336 | 408 | } |
| 337 | 409 | } |
| 338 | 410 | } |
| 339 | 411 | |
| 340 | - fclose($df); | |
| 412 | + fclose($df); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Closes the php://output CSV stream. | |
| 341 | 413 | |
| 342 | 414 | $output = ob_get_clean(); |
| 343 | 415 | |
| 344 | - $filename = sanitize_title(ph_clean($_GET['sub_grid'])) . '-' . date("YmdHis") . '.csv'; | |
| 416 | + $filename = sanitize_title( $sub_grid ) . '-' . gmdate("YmdHis") . '.csv'; | |
| 345 | 417 | |
| 346 | 418 | // disable caching |
| 347 | 419 | $now = gmdate("D, d M Y H:i:s"); |
| 348 | 420 | header("Expires: Tue, 03 Jul 2001 06:00:00 GMT"); |
| @@ -357,8 +429,9 @@ | ||
| 357 | 429 | // disposition / encoding on response body |
| 358 | 430 | header("Content-Disposition: attachment;filename={$filename}"); |
| 359 | 431 | header("Content-Transfer-Encoding: binary"); |
| 360 | 432 | |
| 433 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CSV download produced by fputcsv, not HTML; HTML escaping would corrupt exported field values. | |
| 361 | 434 | echo $output; |
| 362 | 435 | |
| 363 | 436 | die(); |
| 364 | 437 | } |
| @@ -365,12 +438,16 @@ | ||
| 365 | 438 | } |
| 366 | 439 | |
| 367 | 440 | public function export_applicant_list() |
| 368 | 441 | { |
| 369 | - if ( | |
| 370 | - isset($_POST['submitted_applicant_list']) && $_POST['submitted_applicant_list'] == '1' && | |
| 371 | - isset($_POST['export_applicant_list_results']) && $_POST['export_applicant_list_results'] == '1' | |
| 372 | - ) | |
| 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 ) | |
| 373 | 450 | { |
| 374 | 451 | include_once( 'class-ph-admin-applicant-list.php' ); |
| 375 | 452 | $ph_admin_applicant_list = new PH_Admin_Applicant_List(); |
| 376 | 453 | $ph_admin_applicant_list->export(); |
| @@ -380,13 +457,18 @@ | ||
| 380 | 457 | public function record_recently_viewed() |
| 381 | 458 | { |
| 382 | 459 | global $pagenow; |
| 383 | 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 | + | |
| 384 | 466 | if ( |
| 385 | 467 | 'post.php' === $pagenow && |
| 386 | - isset($_GET['post']) && | |
| 468 | + $recent_post_id > 0 && | |
| 387 | 469 | in_array( |
| 388 | - get_post_type((int)$_GET['post']), | |
| 470 | + get_post_type( $recent_post_id ), | |
| 389 | 471 | apply_filters( 'propertyhive_post_types_with_tabs', array('property', 'contact', 'enquiry', 'appraisal', 'viewing', 'offer', 'sale') ) |
| 390 | 472 | ) |
| 391 | 473 | ) |
| 392 | 474 | { |
| @@ -398,27 +480,27 @@ | ||
| 398 | 480 | } |
| 399 | 481 | |
| 400 | 482 | foreach ( $recently_viewed as $time => $post ) |
| 401 | 483 | { |
| 402 | - if ( (int)$_GET['post'] == $post['id'] ) | |
| 484 | + if ( $recent_post_id == $post['id'] ) | |
| 403 | 485 | { |
| 404 | 486 | unset($recently_viewed[$time]); |
| 405 | 487 | } |
| 406 | 488 | } |
| 407 | 489 | |
| 408 | - $title = get_the_title((int)$_GET['post']); | |
| 490 | + $title = get_the_title( $recent_post_id ); | |
| 409 | 491 | |
| 410 | - switch ( get_post_type((int)$_GET['post']) ) | |
| 492 | + switch ( get_post_type( $recent_post_id ) ) | |
| 411 | 493 | { |
| 412 | 494 | case "appraisal": |
| 413 | 495 | { |
| 414 | - $appraisal = new PH_Appraisal( (int)$_GET['post'] ); | |
| 496 | + $appraisal = new PH_Appraisal( $recent_post_id ); | |
| 415 | 497 | $title = $appraisal->get_formatted_summary_address(); |
| 416 | 498 | break; |
| 417 | 499 | } |
| 418 | 500 | case "property": |
| 419 | 501 | { |
| 420 | - $property = new PH_Property( (int)$_GET['post'] ); | |
| 502 | + $property = new PH_Property( $recent_post_id ); | |
| 421 | 503 | $title = $property->get_formatted_summary_address(); |
| 422 | 504 | break; |
| 423 | 505 | } |
| 424 | 506 | case "enquiry": |
| @@ -425,9 +507,9 @@ | ||
| 425 | 507 | case "viewing": |
| 426 | 508 | case "offer": |
| 427 | 509 | case "sale": |
| 428 | 510 | { |
| 429 | - $property_id = get_post_meta( (int)$_GET['post'], '_property_id', TRUE ); | |
| 511 | + $property_id = get_post_meta( $recent_post_id, '_property_id', TRUE ); | |
| 430 | 512 | if ( $property_id != '' ) |
| 431 | 513 | { |
| 432 | 514 | $property = new PH_Property( (int)$property_id ); |
| 433 | 515 | $title = $property->get_formatted_summary_address(); |
| @@ -435,15 +517,15 @@ | ||
| 435 | 517 | break; |
| 436 | 518 | } |
| 437 | 519 | } |
| 438 | 520 | |
| 439 | - $title = ucfirst(get_post_type((int)$_GET['post'])) . ' - ' . $title; | |
| 521 | + $title = ucfirst( get_post_type( $recent_post_id ) ) . ' - ' . $title; | |
| 440 | 522 | |
| 441 | 523 | $recently_viewed = array(time() => array( |
| 442 | - 'id' => (int)$_GET['post'], | |
| 524 | + 'id' => $recent_post_id, | |
| 443 | 525 | 'title' => $title, |
| 444 | - 'post_type' => get_post_type((int)$_GET['post']), | |
| 445 | - 'edit_link' => get_edit_post_link((int)$_GET['post']), | |
| 526 | + 'post_type' => get_post_type( $recent_post_id ), | |
| 527 | + 'edit_link' => get_edit_post_link( $recent_post_id ), | |
| 446 | 528 | )) + $recently_viewed; |
| 447 | 529 | |
| 448 | 530 | $recently_viewed = array_slice($recently_viewed, 0, 10, TRUE); |
| 449 | 531 | |
| @@ -452,11 +534,15 @@ | ||
| 452 | 534 | } |
| 453 | 535 | |
| 454 | 536 | public function admin_dashboard_pages() |
| 455 | 537 | { |
| 456 | - 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 ) | |
| 457 | 543 | { |
| 458 | - switch ( sanitize_title($_GET['page']) ) | |
| 544 | + switch ( $admin_page ) | |
| 459 | 545 | { |
| 460 | 546 | case 'ph-installed': |
| 461 | 547 | { |
| 462 | 548 | add_dashboard_page( |
| @@ -462,9 +548,9 @@ | ||
| 462 | 548 | add_dashboard_page( |
| 463 | 549 | __( 'Welcome to Property Hive', 'propertyhive' ), |
| 464 | 550 | __( 'Welcome to Property Hive', 'propertyhive' ), |
| 465 | 551 | 'manage_propertyhive', |
| 466 | - sanitize_title($_GET['page']), | |
| 552 | + $admin_page, | |
| 467 | 553 | array( $this, 'installed_screen' ) |
| 468 | 554 | ); |
| 469 | 555 | |
| 470 | 556 | break; |
| @@ -545,9 +631,9 @@ | ||
| 545 | 631 | |
| 546 | 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> |
| 547 | 633 | |
| 548 | 634 | <p><strong style="font-size:14px;">Leave a Review</strong><br> |
| 549 | - 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> | |
| 550 | 636 | |
| 551 | 637 | <p><strong style="font-size:14px;">Contribute</strong><br> |
| 552 | 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> |
| 553 | 639 | |
| @@ -655,8 +741,17 @@ | ||
| 655 | 741 | public function review_admin_notices() |
| 656 | 742 | { |
| 657 | 743 | global $wpdb; |
| 658 | 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 | + | |
| 659 | 754 | if ( current_user_can( 'manage_options' ) ) |
| 660 | 755 | { |
| 661 | 756 | $propertyhive_review_prompt_due_timestamp = get_option( 'propertyhive_review_prompt_due_timestamp', 0 ); |
| 662 | 757 | if ( $propertyhive_review_prompt_due_timestamp != '' && $propertyhive_review_prompt_due_timestamp != 0 ) |
| @@ -664,12 +759,12 @@ | ||
| 664 | 759 | if ( $propertyhive_review_prompt_due_timestamp < time() ) |
| 665 | 760 | { |
| 666 | 761 | echo "<div class=\"notice notice-info\" id=\"ph_notice_leave_review\"> |
| 667 | 762 | <p> |
| 668 | - " . __( '<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' ) ) . " | |
| 669 | 764 | </p> |
| 670 | 765 | <p> |
| 671 | - <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> | |
| 672 | 767 | <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_leave_review\">No Thanks</a> |
| 673 | 768 | </p> |
| 674 | 769 | </div>"; |
| 675 | 770 | } |
| @@ -676,15 +771,15 @@ | ||
| 676 | 771 | } |
| 677 | 772 | |
| 678 | 773 | if ( |
| 679 | 774 | class_exists('Easy_Property_Listings') && |
| 680 | - !isset($_GET['plugin_status']) && | |
| 775 | + ! $plugin_status_present && | |
| 681 | 776 | get_option( 'epl_notice_dismissed', '' ) != 'yes' |
| 682 | 777 | ) |
| 683 | 778 | { |
| 684 | 779 | echo "<div class=\"notice notice-error\" id=\"ph_notice_epl\"> |
| 685 | 780 | <p> |
| 686 | - " . __( '<strong>It looks like you\'re also running Easy Property Listings.</strong> This will cause conflicts with Property Hive and should be deactivated.', 'propertyhive' ) . " | |
| 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' ) ) . " | |
| 687 | 782 | </p> |
| 688 | 783 | <p> |
| 689 | 784 | <a href=\"". esc_url(admin_url('plugins.php?s=easy%20property%20listings&plugin_status=all')) . "\" class=\"button-primary\">Deactivate Easy Property Listings</a> |
| 690 | 785 | <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_epl\">Dismiss</a> |
| @@ -697,12 +792,12 @@ | ||
| 697 | 792 | !class_exists('PH_Demo_Data') && |
| 698 | 793 | get_option( 'propertyhive_install_timestamp', '' ) >= 1618268400 && |
| 699 | 794 | get_option( 'propertyhive_hide_demo_data_tab', '' ) != 'yes' && |
| 700 | 795 | ( |
| 701 | - !isset($_GET['page']) | |
| 796 | + ! $admin_page_present | |
| 702 | 797 | || |
| 703 | 798 | ( |
| 704 | - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' && sanitize_title($_GET['page']) != 'ph-settings' | |
| 799 | + $admin_page_present && 'ph-installed' !== $admin_page && 'ph-settings' !== $admin_page | |
| 705 | 800 | ) |
| 706 | 801 | ) |
| 707 | 802 | ) |
| 708 | 803 | { |
| @@ -707,9 +802,9 @@ | ||
| 707 | 802 | ) |
| 708 | 803 | { |
| 709 | 804 | echo "<div class=\"notice notice-info\" id=\"ph_notice_demo_data\"> |
| 710 | 805 | <p> |
| 711 | - " . __( '<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' ) . " | |
| 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' ) ) . " | |
| 712 | 807 | </p> |
| 713 | 808 | <p> |
| 714 | 809 | <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=demo_data')) . "\" class=\"button-primary\">Import Demo Data</a> |
| 715 | 810 | <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_demo_data\">Dismiss</a> |
| @@ -720,12 +815,12 @@ | ||
| 720 | 815 | |
| 721 | 816 | if ( |
| 722 | 817 | get_option('propertyhive_search_results_page_id', '') == '' && |
| 723 | 818 | ( |
| 724 | - !isset($_GET['page']) | |
| 819 | + ! $admin_page_present | |
| 725 | 820 | || |
| 726 | 821 | ( |
| 727 | - 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 | |
| 728 | 823 | ) |
| 729 | 824 | ) && |
| 730 | 825 | get_option( 'missing_search_results_notice_dismissed', '' ) != 'yes' |
| 731 | 826 | ) |
| @@ -731,9 +826,9 @@ | ||
| 731 | 826 | ) |
| 732 | 827 | { |
| 733 | 828 | echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_search_results\"> |
| 734 | 829 | <p> |
| 735 | - " . __( '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' ) . " | |
| 736 | 831 | </p> |
| 737 | 832 | <p> |
| 738 | 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> |
| 739 | 834 | <a href=\"\" class=\"button\" id=\"ph_dismiss_notice_missing_search_results\">" . esc_html(__( 'Dismiss', 'propertyhive' )) . "</a> |
| @@ -745,14 +840,14 @@ | ||
| 745 | 840 | if ( |
| 746 | 841 | get_option('propertyhive_maps_provider') !== 'osm' && |
| 747 | 842 | get_option('propertyhive_maps_provider') !== 'mapbox' && |
| 748 | 843 | get_option('propertyhive_google_maps_api_key', '') == '' && |
| 749 | - !isset($_POST['propertyhive_google_maps_api_key']) && | |
| 844 | + ! $maps_api_key_submitted && | |
| 750 | 845 | ( |
| 751 | - !isset($_GET['page']) | |
| 846 | + ! $admin_page_present | |
| 752 | 847 | || |
| 753 | 848 | ( |
| 754 | - isset($_GET['page']) && sanitize_title($_GET['page']) != 'ph-installed' | |
| 849 | + $admin_page_present && 'ph-installed' !== $admin_page | |
| 755 | 850 | ) |
| 756 | 851 | ) && |
| 757 | 852 | get_option( 'missing_google_maps_api_key_notice_dismissed', '' ) != 'yes' |
| 758 | 853 | ) |
| @@ -760,10 +855,10 @@ | ||
| 760 | 855 | echo "<div class=\"notice notice-info\" id=\"ph_notice_missing_google_maps_api_key\"> |
| 761 | 856 | <p> |
| 762 | 857 | " . sprintf( |
| 763 | 858 | /* translators: %s: URL to plugin settings page where the Google Maps API key can be entered */ |
| 764 | - __( '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' ), | |
| 765 | - admin_url('admin.php?page=ph-settings&tab=general§ion=map') | |
| 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§ion=map') ) | |
| 766 | 861 | ) . " |
| 767 | 862 | </p> |
| 768 | 863 | <p> |
| 769 | 864 | <a href=\"". esc_url(admin_url('admin.php?page=ph-settings&tab=general§ion=map')) . "\" class=\"button-primary\">" . esc_html(__( 'Enter Google Maps API Key', 'propertyhive' )) . "</a> |
| @@ -776,12 +871,12 @@ | ||
| 776 | 871 | if ( |
| 777 | 872 | get_option('propertyhive_license_key', '') != '' && |
| 778 | 873 | get_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ) != 'yes' && |
| 779 | 874 | ( |
| 780 | - !isset($_GET['page']) | |
| 875 | + ! $admin_page_present | |
| 781 | 876 | || |
| 782 | 877 | ( |
| 783 | - 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 | |
| 784 | 879 | ) |
| 785 | 880 | ) |
| 786 | 881 | ) |
| 787 | 882 | { |
| @@ -815,8 +910,9 @@ | ||
| 815 | 910 | $screen = get_current_screen(); |
| 816 | 911 | if ( in_array( $screen->id, array( 'dashboard' ) ) ) |
| 817 | 912 | { |
| 818 | 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. | |
| 819 | 915 | $queuedEmailsExist = (bool)$wpdb->get_var("SELECT 1 FROM " . $wpdb->prefix . "ph_email_log WHERE status = '' LIMIT 1"); |
| 820 | 916 | $cronIsNextScheduled = wp_next_scheduled('propertyhive_process_email_log'); |
| 821 | 917 | if ( $queuedEmailsExist && ( $cronIsNextScheduled === false || $cronIsNextScheduled < strtotime('24 hours ago') ) ) |
| 822 | 918 | { |
| @@ -832,9 +928,9 @@ | ||
| 832 | 928 | } |
| 833 | 929 | } |
| 834 | 930 | } |
| 835 | 931 | |
| 836 | - if ( isset($_GET['propertyhive_contacts_merged']) ) | |
| 932 | + if ( isset( $request_get['propertyhive_contacts_merged'] ) ) | |
| 837 | 933 | { |
| 838 | 934 | echo ' |
| 839 | 935 | <div class="notice notice-info"> |
| 840 | 936 | <p>' . esc_html(__( 'Contacts merged successfully', 'propertyhive' )) . '</p> |
| @@ -853,8 +949,9 @@ | ||
| 853 | 949 | { |
| 854 | 950 | delete_transient( '_ph_activation_redirect' ); |
| 855 | 951 | |
| 856 | 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. | |
| 857 | 954 | if ( is_network_admin() || isset( $_GET['activate-multi'] ) || ! current_user_can( 'manage_propertyhive' ) ) { |
| 858 | 955 | return; |
| 859 | 956 | } |
| 860 | 957 | |
| @@ -872,9 +969,10 @@ | ||
| 872 | 969 | |
| 873 | 970 | // Check role, but also AJAX as request to admin-ajax.php will still need to be made |
| 874 | 971 | if ( !defined( 'DOING_AJAX' ) && $user_role === 'property_hive_contact' ) |
| 875 | 972 | { |
| 876 | - exit( wp_redirect( home_url( '/' ) ) ); | |
| 973 | + wp_safe_redirect( home_url( '/' ) ); | |
| 974 | + exit; | |
| 877 | 975 | } |
| 878 | 976 | } |
| 879 | 977 | |
| 880 | 978 | /** |
| @@ -887,16 +985,26 @@ | ||
| 887 | 985 | global $wpdb; |
| 888 | 986 | |
| 889 | 987 | if ( isset( $_GET['view_propertyhive_email'] ) ) |
| 890 | 988 | { |
| 891 | - 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' ) ) | |
| 892 | 993 | { |
| 893 | - die( 'Security check' ); | |
| 994 | + wp_die( 'Security check' ); | |
| 894 | 995 | } |
| 895 | 996 | |
| 997 | + if ( ! current_user_can( 'manage_propertyhive' ) ) | |
| 998 | + { | |
| 999 | + wp_die( esc_html__( 'Insufficient permissions.', 'propertyhive' ) ); | |
| 1000 | + } | |
| 1001 | + | |
| 896 | 1002 | if ( isset( $_GET['email_id'] ) ) |
| 897 | 1003 | { |
| 898 | - $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 ) ); | |
| 899 | 1007 | if ( null !== $email_log ) |
| 900 | 1008 | { |
| 901 | 1009 | $body = $email_log->body; |
| 902 | 1010 | |
| @@ -904,9 +1012,12 @@ | ||
| 904 | 1012 | { |
| 905 | 1013 | $body = gzuncompress($body); |
| 906 | 1014 | } |
| 907 | 1015 | |
| 908 | - echo apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) ); | |
| 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; | |
| 909 | 1020 | |
| 910 | 1021 | } |
| 911 | 1022 | else |
| 912 | 1023 | { |
| @@ -925,23 +1036,29 @@ | ||
| 925 | 1036 | */ |
| 926 | 1037 | public function preview_emails() { |
| 927 | 1038 | if ( isset( $_GET['preview_propertyhive_email'] ) ) |
| 928 | 1039 | { |
| 929 | - 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' ) ) | |
| 930 | 1044 | { |
| 931 | 1045 | die( 'Security check' ); |
| 932 | 1046 | } |
| 933 | 1047 | |
| 934 | 1048 | $current_user = wp_get_current_user(); |
| 1049 | + $request_get = wp_unslash( $_GET ); | |
| 1050 | + $request_post = wp_unslash( $_POST ); | |
| 935 | 1051 | |
| 936 | 1052 | // get the preview email content |
| 937 | - if ( isset($_GET['property_id']) ) | |
| 1053 | + $email_property_ids = array(); | |
| 1054 | + if ( isset( $request_get['property_id'] ) && is_scalar( $request_get['property_id'] ) ) | |
| 938 | 1055 | { |
| 939 | - $email_property_ids = array((int)$_GET['property_id']); | |
| 1056 | + $email_property_ids = array( absint( $request_get['property_id'] ) ); | |
| 940 | 1057 | } |
| 941 | - elseif ( isset($_POST['email_property_id']) ) | |
| 1058 | + elseif ( isset( $request_post['email_property_id'] ) && is_string( $request_post['email_property_id'] ) ) | |
| 942 | 1059 | { |
| 943 | - $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'] ) ) ) ) ); | |
| 944 | 1061 | } |
| 945 | 1062 | |
| 946 | 1063 | $allowed_tags = array( |
| 947 | 1064 | 'strong' => array(), |
| @@ -962,17 +1079,18 @@ | ||
| 962 | 1079 | ), |
| 963 | 1080 | ); |
| 964 | 1081 | $allowed_tags = apply_filters( 'propertyhive_match_email_allowed_tags', $allowed_tags ); |
| 965 | 1082 | |
| 966 | - $body = wp_kses(wp_unslash($_POST['body']), $allowedposttags); | |
| 1083 | + $raw_body = ( isset( $request_post['body'] ) && is_string( $request_post['body'] ) ) ? $request_post['body'] : ''; | |
| 1084 | + $body = wp_kses( $raw_body, $allowed_tags ); | |
| 967 | 1085 | |
| 968 | - if ( isset($_GET['contact_id']) ) | |
| 1086 | + if ( isset( $request_get['contact_id'] ) && is_scalar( $request_get['contact_id'] ) ) | |
| 969 | 1087 | { |
| 970 | - $contact = new PH_Contact((int)$_GET['contact_id']); | |
| 971 | - $body = str_replace("[contact_name]", $contact->post_title, $body); | |
| 972 | - $body = str_replace("[contact_dear]", $contact->dear(), $body); | |
| 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 ); | |
| 973 | 1091 | } |
| 974 | - $body = str_replace("[property_count]", count($email_property_ids) . ' propert' . ( ( count($email_property_ids) != 1 ) ? 'ies' : 'y' ), $body); | |
| 1092 | + $body = str_replace( '[property_count]', count( $email_property_ids ) . ' propert' . ( ( count( $email_property_ids ) != 1 ) ? 'ies' : 'y' ), $body ); | |
| 975 | 1093 | |
| 976 | 1094 | $office_counts = array(); |
| 977 | 1095 | |
| 978 | 1096 | if ( strpos($body, '[properties]') !== FALSE ) |
| @@ -1013,22 +1131,23 @@ | ||
| 1013 | 1131 | } |
| 1014 | 1132 | |
| 1015 | 1133 | if ( !empty($office_id) ) |
| 1016 | 1134 | { |
| 1017 | - $office_name = get_the_title($office_id); | |
| 1018 | - $office_email_address = get_post_meta( $office_id, '_office_email_address_sales', TRUE ); | |
| 1135 | + $office_name = get_the_title( (int) $office_id ); | |
| 1136 | + $office_email_address = get_post_meta( (int) $office_id, '_office_email_address_sales', TRUE ); | |
| 1019 | 1137 | } |
| 1020 | 1138 | |
| 1021 | - $body = str_replace("[office_name]", $office_name, $body); | |
| 1022 | - $body = str_replace("[office_email_address]", $office_email_address, $body); | |
| 1139 | + $body = str_replace( '[office_name]', esc_html( $office_name ), $body ); | |
| 1140 | + $body = str_replace( '[office_email_address]', esc_html( $office_email_address ), $body ); | |
| 1023 | 1141 | |
| 1024 | - $body = str_replace("[negotiator_name]", $current_user->display_name, $body); | |
| 1025 | - $body = str_replace("[negotiator_email_address]", $current_user->user_email, $body); | |
| 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 ); | |
| 1026 | 1144 | |
| 1027 | 1145 | // wrap the content with the email template and then add styles |
| 1028 | 1146 | $message = apply_filters( 'propertyhive_mail_content', PH()->email->style_inline( PH()->email->wrap_message( $body ) ) ); |
| 1029 | 1147 | |
| 1030 | 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. | |
| 1031 | 1150 | echo $message; |
| 1032 | 1151 | exit; |
| 1033 | 1152 | } |
| 1034 | 1153 | } |