| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | * @package PropertyHive/Classes/ |
| 13 | 13 | * @category Class |
| 14 | 14 | * @author PropertyHive |
| 15 | 15 | */ |
| 16 | +// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Legacy public global class PH_Rest_Api; preserving the existing PH_* class name is required for plugin and extension compatibility. | |
| 16 | 17 | class PH_Rest_Api { |
| 17 | 18 | |
| 18 | 19 | /** @var PH_Rest_Api The single instance of the class */ |
| 19 | 20 | protected static $_instance = null; |
| @@ -18,8 +19,37 @@ | ||
| 18 | 19 | /** @var PH_Rest_Api The single instance of the class */ |
| 19 | 20 | protected static $_instance = null; |
| 20 | 21 | |
| 21 | 22 | /** |
| 23 | + * Download REST media within the site's upload size limit. | |
| 24 | + * | |
| 25 | + * @param string $url Remote media URL. | |
| 26 | + * @return string|WP_Error Temporary filename or an error. | |
| 27 | + */ | |
| 28 | + private static function download_media( $url ) { | |
| 29 | + $temporary_file = wp_tempnam( $url ); | |
| 30 | + if ( ! $temporary_file ) { | |
| 31 | + return new WP_Error( 'propertyhive_media_temp_file', __( 'Unable to create a temporary file.', 'propertyhive' ) ); | |
| 32 | + } | |
| 33 | + $maximum_size = wp_max_upload_size(); | |
| 34 | + $response = wp_safe_remote_get( $url, array( | |
| 35 | + 'timeout' => 30, | |
| 36 | + 'stream' => true, | |
| 37 | + 'filename' => $temporary_file, | |
| 38 | + 'limit_response_size' => $maximum_size + 1, | |
| 39 | + ) ); | |
| 40 | + if ( is_wp_error( $response ) ) { | |
| 41 | + wp_delete_file( $temporary_file ); | |
| 42 | + return $response; | |
| 43 | + } | |
| 44 | + if ( 200 !== wp_remote_retrieve_response_code( $response ) || filesize( $temporary_file ) > $maximum_size ) { | |
| 45 | + wp_delete_file( $temporary_file ); | |
| 46 | + return new WP_Error( 'propertyhive_media_download', __( 'The media could not be downloaded or exceeds the upload size limit.', 'propertyhive' ) ); | |
| 47 | + } | |
| 48 | + return $temporary_file; | |
| 49 | + } | |
| 50 | + | |
| 51 | + /** | |
| 22 | 52 | * Main PH_Rest_Api Instance. |
| 23 | 53 | * |
| 24 | 54 | * Ensures only one instance of PH_Rest_Api is loaded or can be loaded. |
| 25 | 55 | * |
| @@ -39,9 +69,9 @@ | ||
| 39 | 69 | * |
| 40 | 70 | * @since 1.0.0 |
| 41 | 71 | */ |
| 42 | 72 | public function __clone() { |
| 43 | - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' ); | |
| 73 | + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' ); | |
| 44 | 74 | } |
| 45 | 75 | |
| 46 | 76 | /** |
| 47 | 77 | * Unserializing instances of this class is forbidden. |
| @@ -48,9 +78,9 @@ | ||
| 48 | 78 | * |
| 49 | 79 | * @since 1.0.0 |
| 50 | 80 | */ |
| 51 | 81 | public function __wakeup() { |
| 52 | - _doing_it_wrong( __FUNCTION__, __( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' ); | |
| 82 | + _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'propertyhive' ), '1.0.0' ); | |
| 53 | 83 | } |
| 54 | 84 | |
| 55 | 85 | /** |
| 56 | 86 | * Constructor for the licenses class |
| @@ -97,9 +127,9 @@ | ||
| 97 | 127 | } |
| 98 | 128 | |
| 99 | 129 | public function block_enquiry_rest_listing($response, $server, $request) |
| 100 | 130 | { |
| 101 | - if ( $request->get_route() === '/wp/v2/enquiry' ) | |
| 131 | + if ( preg_match( '#^/wp/v2/enquiry(?:/|$)#', $request->get_route() ) ) | |
| 102 | 132 | { |
| 103 | 133 | $current_user = wp_get_current_user(); |
| 104 | 134 | |
| 105 | 135 | if ( !current_user_can('manage_propertyhive') ) |
| @@ -105,9 +135,9 @@ | ||
| 105 | 135 | if ( !current_user_can('manage_propertyhive') ) |
| 106 | 136 | { |
| 107 | 137 | return new WP_Error( |
| 108 | 138 | 'rest_forbidden', |
| 109 | - __('You are not allowed to list enquiries.', 'propertyhive'), | |
| 139 | + __( 'You are not allowed to access enquiries.', 'propertyhive' ), | |
| 110 | 140 | ['status' => 403] |
| 111 | 141 | ); |
| 112 | 142 | } |
| 113 | 143 | |
| @@ -246,22 +276,14 @@ | ||
| 246 | 276 | ), |
| 247 | 277 | )); |
| 248 | 278 | } |
| 249 | 279 | |
| 250 | - public function enquiry_permission_check() | |
| 251 | - { | |
| 252 | - // Check if the user is authenticated | |
| 253 | - if (is_user_logged_in() || apply_filters('rest_authentication_errors', null) === null) { | |
| 254 | - // Check if the user has the capability to create enquiries (e.g., 'edit_posts') | |
| 255 | - if (current_user_can('edit_posts')) { | |
| 256 | - return true; | |
| 257 | - } else { | |
| 258 | - return new WP_Error('rest_forbidden', 'You do not have permissions to create enquiries.', array('status' => 403)); | |
| 259 | - } | |
| 260 | - } else { | |
| 261 | - return new WP_Error('rest_forbidden', 'You are not authenticated.', array('status' => 403)); | |
| 262 | - } | |
| 263 | - } | |
| 280 | + public function enquiry_permission_check() { | |
| 281 | + if ( current_user_can( 'manage_propertyhive' ) ) { | |
| 282 | + return true; | |
| 283 | + } | |
| 284 | + return new WP_Error( 'rest_forbidden', __( 'You do not have permission to create enquiries.', 'propertyhive' ), array( 'status' => 403 ) ); | |
| 285 | + } | |
| 264 | 286 | |
| 265 | 287 | public function handle_enquiry_post(WP_REST_Request $request) |
| 266 | 288 | { |
| 267 | 289 | // Handle the creation of the enquiry post |
| @@ -534,11 +556,13 @@ | ||
| 534 | 556 | |
| 535 | 557 | $PH_Query = new PH_Query(); |
| 536 | 558 | |
| 537 | 559 | // Meta query |
| 560 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- REST property collection arguments are built from PH_Query filters and are consumed by WordPress REST collection pagination. Meta/tax/date filters and fixed ordering are feature behavior; the REST controller bounds per_page and the source preserves request filters. | |
| 538 | 561 | $args['meta_query'] = $PH_Query->get_meta_query(); |
| 539 | 562 | |
| 540 | 563 | // Tax query |
| 564 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- REST property collection arguments are built from PH_Query filters and are consumed by WordPress REST collection pagination. Meta/tax/date filters and fixed ordering are feature behavior; the REST controller bounds per_page and the source preserves request filters. | |
| 541 | 565 | $args['tax_query'] = $PH_Query->get_tax_query(); |
| 542 | 566 | |
| 543 | 567 | // Date query |
| 544 | 568 | $args['date_query'] = $PH_Query->get_date_query(); |
| @@ -546,8 +570,9 @@ | ||
| 546 | 570 | $ordering = $PH_Query->get_search_results_ordering_args(); |
| 547 | 571 | $args['orderby'] = $ordering['orderby'] . ' post_title'; |
| 548 | 572 | $args['order'] = $ordering['order']; |
| 549 | 573 | if ( isset( $ordering['meta_key'] ) ) |
| 574 | + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- REST property collection arguments are built from PH_Query filters and are consumed by WordPress REST collection pagination. Meta/tax/date filters and fixed ordering are feature behavior; the REST controller bounds per_page and the source preserves request filters. | |
| 550 | 575 | $args['meta_key'] = $ordering['meta_key']; |
| 551 | 576 | |
| 552 | 577 | $args = apply_filters( 'propertyhive_rest_api_query_args', $args ); |
| 553 | 578 | |
| @@ -613,8 +638,10 @@ | ||
| 613 | 638 | 'brochures', |
| 614 | 639 | 'epcs', |
| 615 | 640 | 'virtual_tours', |
| 616 | 641 | 'views_total', |
| 642 | + 'views_today', | |
| 643 | + 'views_yesterday', | |
| 617 | 644 | 'views_last_7_days', |
| 618 | 645 | 'views_last_14_days', |
| 619 | 646 | 'views_last_30_days', |
| 620 | 647 | ); |
| @@ -851,8 +878,10 @@ | ||
| 851 | 878 | { |
| 852 | 879 | $return = $property->get_virtual_tours(); |
| 853 | 880 | break; |
| 854 | 881 | } |
| 882 | + case "views_today": | |
| 883 | + case "views_yesterday": | |
| 855 | 884 | case "views_last_7_days": |
| 856 | 885 | case "views_last_14_days": |
| 857 | 886 | case "views_last_30_days": |
| 858 | 887 | case "views_total": |
| @@ -857,35 +886,68 @@ | ||
| 857 | 886 | case "views_last_30_days": |
| 858 | 887 | case "views_total": |
| 859 | 888 | { |
| 860 | 889 | $view_statistics = $property->_view_statistics; |
| 861 | - if ( !is_array($view_statistics) ) | |
| 862 | - { | |
| 863 | - $view_statistics = array(); | |
| 864 | - } | |
| 890 | + if ( !is_array($view_statistics) ) | |
| 891 | + { | |
| 892 | + $view_statistics = array(); | |
| 893 | + } | |
| 865 | 894 | |
| 866 | - $views = 0; | |
| 895 | + $views = 0; | |
| 867 | 896 | |
| 868 | - $date_from = '2001-01-01'; | |
| 869 | - switch ($field_name) | |
| 870 | - { | |
| 871 | - case "views_last_7_days": { $date_from = date("Y-m-d", strtotime('7 days ago')); break; } | |
| 872 | - case "views_last_14_days": { $date_from = date("Y-m-d", strtotime('14 days ago')); break; } | |
| 873 | - case "views_last_30_days": { $date_from = date("Y-m-d", strtotime('30 days ago')); break; } | |
| 874 | - } | |
| 875 | - $date_from = strtotime($date_from); | |
| 897 | + $today = current_datetime()->setTime(0, 0, 0); | |
| 876 | 898 | |
| 877 | - $date_to = date("Y-m-d"); | |
| 878 | - $date_to = strtotime($date_to); | |
| 899 | + $date_from = new DateTimeImmutable('2001-01-01', wp_timezone()); | |
| 900 | + $date_to = $today; | |
| 879 | 901 | |
| 880 | - for ($i = $date_from; $i <= $date_to; $i += 86400) | |
| 881 | - { | |
| 882 | - if ( isset($view_statistics[date("Y-m-d", $i)]) ) | |
| 883 | - { | |
| 884 | - $views += $view_statistics[date("Y-m-d", $i)]; | |
| 885 | - } | |
| 886 | - } | |
| 902 | + switch ($field_name) | |
| 903 | + { | |
| 904 | + case 'views_last_7_days': | |
| 905 | + { | |
| 906 | + $date_from = $today->modify('-7 days'); | |
| 907 | + $date_to = $today->modify('-1 day'); | |
| 908 | + break; | |
| 909 | + } | |
| 887 | 910 | |
| 911 | + case 'views_last_14_days': | |
| 912 | + { | |
| 913 | + $date_from = $today->modify('-14 days'); | |
| 914 | + $date_to = $today->modify('-1 day'); | |
| 915 | + break; | |
| 916 | + } | |
| 917 | + | |
| 918 | + case 'views_last_30_days': | |
| 919 | + { | |
| 920 | + $date_from = $today->modify('-30 days'); | |
| 921 | + $date_to = $today->modify('-1 day'); | |
| 922 | + break; | |
| 923 | + } | |
| 924 | + | |
| 925 | + case 'views_today': | |
| 926 | + { | |
| 927 | + $date_from = $today; | |
| 928 | + $date_to = $today; | |
| 929 | + break; | |
| 930 | + } | |
| 931 | + | |
| 932 | + case 'views_yesterday': | |
| 933 | + { | |
| 934 | + $date_from = $today->modify('-1 day'); | |
| 935 | + $date_to = $date_from; | |
| 936 | + break; | |
| 937 | + } | |
| 938 | + } | |
| 939 | + | |
| 940 | + for ( $date = $date_from; $date <= $date_to; $date = $date->modify('+1 day') ) | |
| 941 | + { | |
| 942 | + $date_key = $date->format('Y-m-d'); | |
| 943 | + | |
| 944 | + if ( isset($view_statistics[$date_key]) ) | |
| 945 | + { | |
| 946 | + $views += (int)$view_statistics[$date_key]; | |
| 947 | + } | |
| 948 | + } | |
| 949 | + | |
| 888 | 950 | $return = $views; |
| 889 | 951 | break; |
| 890 | 952 | } |
| 891 | 953 | default: |
| @@ -952,8 +1014,12 @@ | ||
| 952 | 1014 | break; |
| 953 | 1015 | } |
| 954 | 1016 | case "description": |
| 955 | 1017 | { |
| 1018 | + if ( ! is_string( $value ) ) { | |
| 1019 | + return new WP_Error( 'propertyhive_rest_description_invalid', __( 'The description must be text.', 'propertyhive' ), array( 'status' => 400 ) ); | |
| 1020 | + } | |
| 1021 | + $value = propertyhive_sanitize_description( $value ); | |
| 956 | 1022 | $property = new PH_Property($object->ID); |
| 957 | 1023 | |
| 958 | 1024 | if ( isset($property->_department) && $property->_department == 'commercial' ) |
| 959 | 1025 | { |
| @@ -958,9 +1024,9 @@ | ||
| 958 | 1024 | if ( isset($property->_department) && $property->_department == 'commercial' ) |
| 959 | 1025 | { |
| 960 | 1026 | update_post_meta( $object->ID, '_descriptions', '1' ); |
| 961 | 1027 | update_post_meta( $object->ID, '_description_name_0', '' ); |
| 962 | - update_post_meta( $object->ID, '_description_0', $value ); | |
| 1028 | + update_post_meta( $object->ID, '_description_0', wp_slash( $value ) ); | |
| 963 | 1029 | } |
| 964 | 1030 | else |
| 965 | 1031 | { |
| 966 | 1032 | update_post_meta( $object->ID, '_rooms', '1' ); |
| @@ -965,9 +1031,9 @@ | ||
| 965 | 1031 | { |
| 966 | 1032 | update_post_meta( $object->ID, '_rooms', '1' ); |
| 967 | 1033 | update_post_meta( $object->ID, '_room_name_0', '' ); |
| 968 | 1034 | update_post_meta( $object->ID, '_room_dimensions_0', '' ); |
| 969 | - update_post_meta( $object->ID, '_room_description_0', $value ); | |
| 1035 | + update_post_meta( $object->ID, '_room_description_0', wp_slash( $value ) ); | |
| 970 | 1036 | } |
| 971 | 1037 | break; |
| 972 | 1038 | } |
| 973 | 1039 | case "images": |
| @@ -974,8 +1040,20 @@ | ||
| 974 | 1040 | case "floorplans": |
| 975 | 1041 | case "brochures": |
| 976 | 1042 | case "epcs": |
| 977 | 1043 | { |
| 1044 | + if ( ! current_user_can( 'upload_files' ) || ! current_user_can( 'edit_post', $object->ID ) ) { | |
| 1045 | + return new WP_Error( 'propertyhive_rest_media_forbidden', __( 'You do not have permission to update property media.', 'propertyhive' ), array( 'status' => 403 ) ); | |
| 1046 | + } | |
| 1047 | + if ( ! is_array( $value ) || count( $value ) > 100 ) { | |
| 1048 | + return new WP_Error( 'propertyhive_rest_media_invalid', __( 'Supply an array containing no more than 100 media items.', 'propertyhive' ), array( 'status' => 400 ) ); | |
| 1049 | + } | |
| 1050 | + foreach ( $value as $media_item ) { | |
| 1051 | + if ( ! is_array( $media_item ) || ! isset( $media_item['url'] ) || ! is_string( $media_item['url'] ) || ! preg_match( '~^(https?:)?//~i', $media_item['url'] ) ) { | |
| 1052 | + return new WP_Error( 'propertyhive_rest_media_invalid', __( 'Each media item must contain an HTTP or HTTPS URL.', 'propertyhive' ), array( 'status' => 400 ) ); | |
| 1053 | + } | |
| 1054 | + } | |
| 1055 | + | |
| 978 | 1056 | if ( !function_exists('media_handle_upload') ) { |
| 979 | 1057 | require_once(ABSPATH . "wp-admin" . '/includes/image.php'); |
| 980 | 1058 | require_once(ABSPATH . "wp-admin" . '/includes/file.php'); |
| 981 | 1059 | require_once(ABSPATH . "wp-admin" . '/includes/media.php'); |
| @@ -1049,9 +1127,9 @@ | ||
| 1049 | 1127 | $media_ids[] = $imported_previously_id; |
| 1050 | 1128 | } |
| 1051 | 1129 | else |
| 1052 | 1130 | { |
| 1053 | - $tmp = download_url( $url ); | |
| 1131 | + $tmp = self::download_media( $url ); | |
| 1054 | 1132 | $file_array = array( |
| 1055 | 1133 | 'name' => basename( $url ), |
| 1056 | 1134 | 'tmp_name' => $tmp |
| 1057 | 1135 | ); |
| @@ -1058,9 +1136,9 @@ | ||
| 1058 | 1136 | |
| 1059 | 1137 | // Check for download errors |
| 1060 | 1138 | if ( is_wp_error( $tmp ) ) |
| 1061 | 1139 | { |
| 1062 | - // ERROR: $tmp->get_error_message(); | |
| 1140 | + return $tmp; | |
| 1063 | 1141 | } |
| 1064 | 1142 | else |
| 1065 | 1143 | { |
| 1066 | 1144 | $id = media_handle_sideload( $file_array, $object->ID, $description, array('post_title' => $filename) ); |
| @@ -1067,11 +1145,11 @@ | ||
| 1067 | 1145 | |
| 1068 | 1146 | // Check for handle sideload errors. |
| 1069 | 1147 | if ( is_wp_error( $id ) ) |
| 1070 | 1148 | { |
| 1071 | - @unlink( $file_array['tmp_name'] ); | |
| 1149 | + wp_delete_file( $file_array['tmp_name'] ); | |
| 1072 | 1150 | |
| 1073 | - // ERROR: $id->get_error_message(); | |
| 1151 | + return $id; | |
| 1074 | 1152 | } |
| 1075 | 1153 | else |
| 1076 | 1154 | { |
| 1077 | 1155 | $media_ids[] = $id; |
| @@ -1088,9 +1166,9 @@ | ||
| 1088 | 1166 | if ( is_array($previous_media_ids) && !empty($previous_media_ids) ) |
| 1089 | 1167 | { |
| 1090 | 1168 | foreach ( $previous_media_ids as $previous_media_id ) |
| 1091 | 1169 | { |
| 1092 | - if ( !in_array($previous_media_id, $media_ids) ) | |
| 1170 | + if ( ! in_array( $previous_media_id, $media_ids ) && current_user_can( 'delete_post', $previous_media_id ) ) | |
| 1093 | 1171 | { |
| 1094 | 1172 | if ( wp_delete_attachment( $previous_media_id, TRUE ) !== FALSE ) |
| 1095 | 1173 | { |
| 1096 | 1174 | |
| @@ -1230,5 +1308,4 @@ | ||
| 1230 | 1308 | } |
| 1231 | 1309 | } |
| 1232 | 1310 | |
| 1233 | 1311 | } |
| 1234 | - | |