| @@ -42,9 +42,9 @@ | ||
| 42 | 42 | * @var array|false |
| 43 | 43 | */ |
| 44 | 44 | private $booking_hash_scope; |
| 45 | 45 | |
| 46 | - public function __construct(){// $bookings, $booking_types ) { | |
| 46 | + public function __construct(){// $bookings, $booking_types ) { | |
| 47 | 47 | |
| 48 | 48 | $this->reset_data_in_previous_cell(); |
| 49 | 49 | $this->booking_hash_scope = false; |
| 50 | 50 | |
| @@ -125,8 +125,116 @@ | ||
| 125 | 125 | |
| 126 | 126 | } |
| 127 | 127 | |
| 128 | 128 | /** |
| 129 | + * Validate the server-generated DOM identifier used by Timeline navigation. | |
| 130 | + * | |
| 131 | + * Public AJAX requests may return this value in JavaScript and inline event | |
| 132 | + * attributes. Accepting only the established prefix and decimal suffix keeps | |
| 133 | + * it an identifier rather than caller-controlled markup or selector syntax. | |
| 134 | + * | |
| 135 | + * @param mixed $html_client_id Candidate Timeline DOM identifier. | |
| 136 | + * @return string Valid identifier, or an empty string. | |
| 137 | + */ | |
| 138 | + public static function normalize_html_client_id( $html_client_id ) { | |
| 139 | + if ( ! is_scalar( $html_client_id ) ) { | |
| 140 | + return ''; | |
| 141 | + } | |
| 142 | + | |
| 143 | + $html_client_id = (string) $html_client_id; | |
| 144 | + if ( 64 < strlen( $html_client_id ) ) { | |
| 145 | + return ''; | |
| 146 | + } | |
| 147 | + | |
| 148 | + return preg_match( '/\Awpbc_timeline_[0-9]+\z/D', $html_client_id ) | |
| 149 | + ? $html_client_id | |
| 150 | + : ''; | |
| 151 | + } | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * Normalize the public timeline options contract. | |
| 155 | + * | |
| 156 | + * Timeline navigation round-trips options through the browser. Only Resource | |
| 157 | + * links are consumed by the renderer, so every other key is discarded rather | |
| 158 | + * than retained as attacker-controlled state for a later response. | |
| 159 | + * | |
| 160 | + * @param mixed $options Candidate timeline options. | |
| 161 | + * @return array<string,array<int,string>> Valid Resource links keyed by Resource ID. | |
| 162 | + */ | |
| 163 | + public static function normalize_options( $options ) { | |
| 164 | + if ( | |
| 165 | + ! is_array( $options ) | |
| 166 | + || empty( $options['resource_link'] ) | |
| 167 | + || ! is_array( $options['resource_link'] ) | |
| 168 | + ) { | |
| 169 | + return array(); | |
| 170 | + } | |
| 171 | + | |
| 172 | + $resource_links = array(); | |
| 173 | + foreach ( $options['resource_link'] as $resource_key => $resource_url ) { | |
| 174 | + if ( ! is_scalar( $resource_key ) || ! is_scalar( $resource_url ) ) { | |
| 175 | + continue; | |
| 176 | + } | |
| 177 | + | |
| 178 | + $resource_id = absint( $resource_key ); | |
| 179 | + $resource_url = esc_url_raw( (string) $resource_url ); | |
| 180 | + if ( empty( $resource_id ) || '' === $resource_url ) { | |
| 181 | + continue; | |
| 182 | + } | |
| 183 | + | |
| 184 | + $resource_links[ $resource_id ] = $resource_url; | |
| 185 | + } | |
| 186 | + | |
| 187 | + return empty( $resource_links ) | |
| 188 | + ? array() | |
| 189 | + : array( 'resource_link' => $resource_links ); | |
| 190 | + } | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * Decode and normalize browser-submitted timeline options. | |
| 194 | + * | |
| 195 | + * @param mixed $encoded_options JSON text received from the timeline client. | |
| 196 | + * @return array<string,array<int,string>> Valid Resource links, or an empty array. | |
| 197 | + */ | |
| 198 | + public static function decode_options( $encoded_options ) { | |
| 199 | + if ( ! is_scalar( $encoded_options ) ) { | |
| 200 | + return array(); | |
| 201 | + } | |
| 202 | + | |
| 203 | + $decoded_options = json_decode( (string) $encoded_options, true, 32 ); | |
| 204 | + if ( JSON_ERROR_NONE !== json_last_error() ) { | |
| 205 | + return array(); | |
| 206 | + } | |
| 207 | + | |
| 208 | + return self::normalize_options( $decoded_options ); | |
| 209 | + } | |
| 210 | + | |
| 211 | + /** | |
| 212 | + * Encode timeline options as one complete JavaScript string literal. | |
| 213 | + * | |
| 214 | + * The timeline browser contract stores JSON text, rather than an object, in | |
| 215 | + * `timeline_obj.options`. Encoding the normalized options twice preserves that | |
| 216 | + * contract while the hexadecimal flags prevent quotes or HTML delimiters in a | |
| 217 | + * URL from terminating the inline script context. | |
| 218 | + * | |
| 219 | + * @param mixed $options Candidate timeline options. | |
| 220 | + * @return string JavaScript-safe JSON string literal, including its delimiters. | |
| 221 | + */ | |
| 222 | + public static function encode_options_for_inline_script( $options ) { | |
| 223 | + $options_json = wp_json_encode( self::normalize_options( $options ) ); | |
| 224 | + if ( false === $options_json ) { | |
| 225 | + $options_json = '{}'; | |
| 226 | + } | |
| 227 | + | |
| 228 | + $javascript_literal = wp_json_encode( | |
| 229 | + $options_json, | |
| 230 | + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | |
| 231 | + ); | |
| 232 | + | |
| 233 | + return false === $javascript_literal ? '"{}"' : $javascript_literal; | |
| 234 | + } | |
| 235 | + | |
| 236 | + /** | |
| 129 | 237 | * Apply an exact booking and resource authorization scope to timeline SQL arguments. |
| 130 | 238 | * |
| 131 | 239 | * A booking hash is a bearer credential for one booking. It must never be |
| 132 | 240 | * converted into a customer-data keyword or used to broaden the query. Invalid |
| @@ -225,9 +333,9 @@ | ||
| 225 | 333 | |
| 226 | 334 | $this->is_frontend = true; |
| 227 | 335 | |
| 228 | 336 | // FixIn: 7.0.1.50. |
| 229 | - if ( isset( $attr['options'] ) ) { | |
| 337 | + if ( isset( $attr['options'] ) ) { | |
| 230 | 338 | |
| 231 | 339 | $shortcode_param__options = $attr['options']; |
| 232 | 340 | $shortcode_param__options = html_entity_decode( $shortcode_param__options ); // FixIn: 9.8.15.6. |
| 233 | 341 | $custom_params = array(); |
| @@ -251,11 +359,12 @@ | ||
| 251 | 359 | $this->options[ $matche_value[1] ][ $matche_value[2] ] = $matche_value[3]; |
| 252 | 360 | } |
| 253 | 361 | } |
| 254 | 362 | |
| 255 | -//debuge($this->options); | |
| 256 | - } | |
| 257 | - // FixIn: 7.0.1.50. | |
| 363 | +//debuge($this->options); | |
| 364 | + } | |
| 365 | + $this->options = self::normalize_options( $this->options ); | |
| 366 | + // FixIn: 7.0.1.50. | |
| 258 | 367 | |
| 259 | 368 | |
| 260 | 369 | //Ovverride some parameters |
| 261 | 370 | //if ( isset( $attr['resource_id'] ) ) { $attr['type'] = $attr['resource_id']; } |
| @@ -531,9 +640,9 @@ | ||
| 531 | 640 | $this->dates_array = $bookings_date_time[0]; |
| 532 | 641 | $this->time_array_new = $bookings_date_time[1]; |
| 533 | 642 | |
| 534 | 643 | |
| 535 | - $this->html_client_id = $attr['html_client_id']; | |
| 644 | + $this->html_client_id = self::normalize_html_client_id( $attr['html_client_id'] ); | |
| 536 | 645 | |
| 537 | 646 | return $this->html_client_id; |
| 538 | 647 | } |
| 539 | 648 | |
| @@ -589,9 +698,12 @@ | ||
| 589 | 698 | 'header_title' : "<?php echo esc_js( $this->timeline_titles['header_title'] ); ?>", |
| 590 | 699 | 'wh_trash' : "<?php echo esc_js( $this->request_args['wh_trash'] ); ?>", |
| 591 | 700 | 'limit_hours' : "<?php echo esc_js( $this->request_args['limit_hours'] ); ?>", |
| 592 | 701 | 'only_booked_resources': "<?php echo esc_js( $this->request_args['only_booked_resources'] ); ?>", |
| 593 | - 'options' : '<?php echo wp_json_encode( $this->options ); ?>', | |
| 702 | + 'options' : <?php | |
| 703 | + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Returns a complete JSON-encoded JavaScript string literal. | |
| 704 | + echo self::encode_options_for_inline_script( $this->options ); | |
| 705 | + ?>, | |
| 594 | 706 | 'booking_hash' : "<?php echo esc_js( $this->request_args['booking_hash'] ); ?>" |
| 595 | 707 | }; |
| 596 | 708 | </script> |
| 597 | 709 | <div class="flex_tl_nav"> |
| @@ -914,11 +1026,11 @@ | ||
| 914 | 1026 | } // FixIn: 7.0.1.14. |
| 915 | 1027 | if ( isset( $param['booking_hash'] ) ) { |
| 916 | 1028 | $this->request_args['booking_hash'] = $param['booking_hash']; |
| 917 | 1029 | } // FixIn: 8.1.3.5. |
| 918 | - if ( ( empty( $this->options ) ) && ( isset( $param['options'] ) ) ) { | |
| 919 | - $this->options = json_decode( wp_unslash( $param['options'] ), true ); // FixIn: 9.2.1.8. | |
| 920 | - } | |
| 1030 | + if ( ( empty( $this->options ) ) && ( isset( $param['options'] ) ) ) { | |
| 1031 | + $this->options = self::decode_options( $param['options'] ); // FixIn: 9.2.1.8. | |
| 1032 | + } | |
| 921 | 1033 | |
| 922 | 1034 | } |
| 923 | 1035 | |
| 924 | 1036 | |
| @@ -1927,11 +2039,9 @@ | ||
| 1927 | 2039 | $bk_title .= " \n" . $this->get_booking_title_for_timeline( $booking_id, $row_settings['bookings'] ); |
| 1928 | 2040 | |
| 1929 | 2041 | $bk_title .= " \n" . wp_strip_all_tags( wpbc_get_short_dates_formated_to_show( $row_settings['bookings'][ $booking_id ]->dates_short ) ) ; |
| 1930 | 2042 | |
| 1931 | - if ( function_exists( 'wpbc_is_11_5_features_enabled' ) && wpbc_is_11_5_features_enabled() ) { | |
| 1932 | - $bk_title = apply_filters( 'wpbc_timeline_booking_pipeline_title', $bk_title, $booking_id, $row_settings['bookings'] ); | |
| 1933 | - } | |
| 2043 | + $bk_title = apply_filters( 'wpbc_timeline_booking_pipeline_title', $bk_title, $booking_id, $row_settings['bookings'] ); | |
| 1934 | 2044 | |
| 1935 | 2045 | ?><a href="javascript:void(0)" |
| 1936 | 2046 | class="in_cell_date_booking_pipeline_a" |
| 1937 | 2047 | title="<?php echo esc_attr( $bk_title ); ?>" |
| @@ -3076,35 +3186,38 @@ | ||
| 3076 | 3186 | // Link |
| 3077 | 3187 | $header_title .= '<a class=\'button button-secondary\' |
| 3078 | 3188 | title=\'' . esc_attr( str_replace( "'", '', __( 'Booking Listing', 'booking' ) ) ) . '\' |
| 3079 | 3189 | href=\''.wpbc_get_bookings_url( true, false ).'&wh_booking_id='.$bk_id.'&tab=vm_booking_listing\' ><i class=\'wpbc_icn_gps_fixed\'></i></a>'; |
| 3080 | - //Edit | |
| 3081 | - if ( class_exists( 'wpdev_bk_personal' ) ) { | |
| 3082 | - $bk_url_add = wpbc_get_new_booking_url( true, false ); | |
| 3083 | - $bk_hash = (isset( $bookings[$bk_id]->hash )) ? $bookings[$bk_id]->hash : ''; | |
| 3084 | - $bk_booking_type = $bookings[$bk_id]->booking_type; | |
| 3085 | - $edit_booking_url = $bk_url_add . '&booking_type=' . $bk_booking_type . '&booking_hash=' . $bk_hash . '&parent_res=1'; | |
| 3086 | - // FixIn: 10.10.1.2 $edit_booking_url .= ( 'Off' !== get_bk_option( 'booking_is_resource_no_update__during_editing' ) ) ? '&resource_no_update=1' : ''; // FixIn: 9.4.2.3. | |
| 3087 | - | |
| 3088 | - $custom_booking_form = ''; | |
| 3089 | - if ( ! empty( $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form'] ) ) { | |
| 3090 | - $custom_booking_form = $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form']; | |
| 3091 | - $edit_booking_url .= '&booking_form=' . rawurlencode( $custom_booking_form ); // FixIn: 9.4.3.12. | |
| 3092 | - } | |
| 3093 | - | |
| 3094 | - $edit_booking_onclick = "if ( 'function' === typeof wpbc_boo_listing__click__add_booking_modal_from_row ) {" | |
| 3095 | - . ' wpbc_boo_listing__click__add_booking_modal_from_row(' | |
| 3096 | - . absint( $bk_id ) . ',' | |
| 3190 | + //Edit | |
| 3191 | + if ( class_exists( 'wpdev_bk_personal' ) ) { | |
| 3192 | + $bk_hash = (isset( $bookings[$bk_id]->hash )) ? $bookings[$bk_id]->hash : ''; | |
| 3193 | + $bk_booking_type = $bookings[$bk_id]->booking_type; | |
| 3194 | + // FixIn: 10.10.1.2 $edit_booking_url .= ( 'Off' !== get_bk_option( 'booking_is_resource_no_update__during_editing' ) ) ? '&resource_no_update=1' : ''; // FixIn: 9.4.2.3. | |
| 3195 | + | |
| 3196 | + $custom_booking_form = ''; | |
| 3197 | + if ( ! empty( $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form'] ) ) { | |
| 3198 | + $custom_booking_form = $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form']; | |
| 3199 | + } | |
| 3200 | + $edit_booking_url = wpbc_get_booking_admin_edit_url( $bk_booking_type, $bk_hash, $custom_booking_form ); | |
| 3201 | + | |
| 3202 | + $edit_booking_onclick = ''; | |
| 3203 | + if ( ! wpbc_is_booking_admin_edit_page_enabled() ) { | |
| 3204 | + $edit_booking_onclick = "if ( 'function' === typeof wpbc_boo_listing__click__add_booking_modal_from_row ) {" | |
| 3205 | + . ' wpbc_boo_listing__click__add_booking_modal_from_row(' | |
| 3206 | + . absint( $bk_id ) . ',' | |
| 3097 | 3207 | . absint( $bk_booking_type ) . ',' |
| 3098 | 3208 | . "'" . esc_js( $bk_hash ) . "'," |
| 3099 | 3209 | . "'" . esc_js( $custom_booking_form ) . "'" |
| 3100 | - . ' ); return false; }'; | |
| 3210 | + . ' ); return false; }'; | |
| 3211 | + } | |
| 3212 | + | |
| 3213 | + $header_title .= '<a class=\'button button-secondary\' | |
| 3214 | + title=\'' . esc_attr( str_replace( "'", '', __( 'Edit', 'booking' ) ) ) . '\' | |
| 3215 | + href=\'' . esc_url( $edit_booking_url ) . '\'' | |
| 3216 | + . ( '' !== $edit_booking_onclick ? ' onclick=\'' . esc_attr( $edit_booking_onclick ) . '\'' : '' ) | |
| 3217 | + . ' ><i class=\'wpbc_icn_draw\'></i></a>'; | |
| 3101 | 3218 | |
| 3102 | - $header_title .= '<a class=\'button button-secondary\' | |
| 3103 | - title=\'' . esc_attr( str_replace( "'", '', __( 'Edit', 'booking' ) ) ) . '\' | |
| 3104 | - href=\'' . esc_url( $edit_booking_url ) . '\' onclick=\'' . esc_attr( $edit_booking_onclick ) . '\' ><i class=\'wpbc_icn_draw\'></i></a>'; | |
| 3105 | 3219 | |
| 3106 | - | |
| 3107 | 3220 | $header_title .= '<span class=\'wpbc-buttons-separator\'></span>'; |
| 3108 | 3221 | } |
| 3109 | 3222 | // Trash |
| 3110 | 3223 | //$header_title .= '<a class=\'button button-secondary\' href=\'javascript:;\' onclick=\'javascript:delete_booking(' . $bk_id . ', ' . $this->current_user_id . ', "' . wpbc_get_maybe_reloaded_booking_locale() . '" , 1 );\' ><i class=\'wpbc_icn_delete_outline\'></i></a>'; |
| @@ -3315,11 +3428,9 @@ | ||
| 3315 | 3428 | 'title' => $header_title, |
| 3316 | 3429 | 'content' => $content_text |
| 3317 | 3430 | ); |
| 3318 | 3431 | |
| 3319 | - if ( function_exists( 'wpbc_is_11_5_features_enabled' ) && wpbc_is_11_5_features_enabled() ) { | |
| 3320 | - $popover = apply_filters( 'wpbc_timeline_booking_popover', $popover, $bk_id, $bookings, $this->is_frontend ); | |
| 3321 | - } | |
| 3432 | + $popover = apply_filters( 'wpbc_timeline_booking_popover', $popover, $bk_id, $bookings, $this->is_frontend ); | |
| 3322 | 3433 | |
| 3323 | 3434 | return $popover; |
| 3324 | 3435 | } |
| 3325 | 3436 | |
| @@ -3382,9 +3493,17 @@ | ||
| 3382 | 3493 | $clean_key = sanitize_key( wp_unslash( (string) $tl_key ) ); |
| 3383 | 3494 | if ( '' === $clean_key || ! isset( $allowed_timeline_keys[ $clean_key ] ) ) { |
| 3384 | 3495 | continue; |
| 3385 | 3496 | } |
| 3386 | - $attr[ $clean_key ] = wpbc_clean_text_value( wp_unslash( (string) $tl_value ) ); | |
| 3497 | + $clean_value = wp_unslash( (string) $tl_value ); | |
| 3498 | + if ( 'options' === $clean_key ) { | |
| 3499 | + $normalized_options = WPBC_TimelineFlex::decode_options( $clean_value ); | |
| 3500 | + $encoded_options = wp_json_encode( $normalized_options ); | |
| 3501 | + $attr[ $clean_key ] = false === $encoded_options ? '{}' : $encoded_options; | |
| 3502 | + continue; | |
| 3503 | + } | |
| 3504 | + | |
| 3505 | + $attr[ $clean_key ] = wpbc_clean_text_value( $clean_value ); | |
| 3387 | 3506 | } |
| 3388 | 3507 | |
| 3389 | 3508 | // phpcs:ignore WordPress.Security.NonceVerification.Missing |
| 3390 | 3509 | if ( isset( $_POST['nav_step'] ) && ! is_scalar( $_POST['nav_step'] ) ) { |
| @@ -3391,11 +3510,16 @@ | ||
| 3391 | 3510 | status_header( 400 ); |
| 3392 | 3511 | wp_die( '' ); |
| 3393 | 3512 | } |
| 3394 | 3513 | |
| 3395 | - $attr['nav_step'] = isset( $_POST['nav_step'] ) ? wpbc_clean_text_value( wp_unslash( (string) $_POST['nav_step'] ) ) : '0'; // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 3396 | - $attr['is_frontend'] = isset( $attr['is_frontend'] ) ? $attr['is_frontend'] : '1'; | |
| 3397 | - if ( empty( $attr['html_client_id'] ) ) { | |
| 3514 | + $attr['nav_step'] = isset( $_POST['nav_step'] ) | |
| 3515 | + ? wpbc_clean_text_value( wp_unslash( (string) $_POST['nav_step'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing | |
| 3516 | + : '0'; | |
| 3517 | + $attr['is_frontend'] = isset( $attr['is_frontend'] ) ? $attr['is_frontend'] : '1'; | |
| 3518 | + $attr['html_client_id'] = isset( $attr['html_client_id'] ) | |
| 3519 | + ? WPBC_TimelineFlex::normalize_html_client_id( $attr['html_client_id'] ) | |
| 3520 | + : ''; | |
| 3521 | + if ( '' === $attr['html_client_id'] ) { | |
| 3398 | 3522 | status_header( 400 ); |
| 3399 | 3523 | wp_die( '' ); |
| 3400 | 3524 | } |
| 3401 | 3525 | |