PluginProbe
Booking Calendar / 11.9
Booking Calendar v11.9
11.9 11.8.4 11.8.3 11.8.2 11.8.1 11.8 11.7 11.6.1 11.6 11.5 11.4.3 11.4.2 11.4.1 11.4 11.3 11.2.1 11.2 11.1 11.0 10.15.7 10.15.6 10.1.3 10.10 10.10.1 10.10.2 All 205 releases
← All changes | core/timeline/v2/wpbc-class-timeline_v2.php +164 -36 11.6 → 11.9 View file →
@@ -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
@@ -3074,35 +3186,38 @@
3074 3186 // Link
3075 3187 $header_title .= '<a class=\'button button-secondary\'
3076 3188 title=\'' . esc_attr( str_replace( "'", '', __( 'Booking Listing', 'booking' ) ) ) . '\'
3077 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>';
3078 - //Edit
3079 - if ( class_exists( 'wpdev_bk_personal' ) ) {
3080 - $bk_url_add = wpbc_get_new_booking_url( true, false );
3081 - $bk_hash = (isset( $bookings[$bk_id]->hash )) ? $bookings[$bk_id]->hash : '';
3082 - $bk_booking_type = $bookings[$bk_id]->booking_type;
3083 - $edit_booking_url = $bk_url_add . '&booking_type=' . $bk_booking_type . '&booking_hash=' . $bk_hash . '&parent_res=1';
3084 - // 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.
3085 -
3086 - $custom_booking_form = '';
3087 - if ( ! empty( $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form'] ) ) {
3088 - $custom_booking_form = $bookings[ $bk_id ]->form_data['_all_fields_']['wpbc_custom_booking_form'];
3089 - $edit_booking_url .= '&booking_form=' . rawurlencode( $custom_booking_form ); // FixIn: 9.4.3.12.
3090 - }
3091 -
3092 - $edit_booking_onclick = "if ( 'function' === typeof wpbc_boo_listing__click__add_booking_modal_from_row ) {"
3093 - . ' wpbc_boo_listing__click__add_booking_modal_from_row('
3094 - . 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 ) . ','
3095 3207 . absint( $bk_booking_type ) . ','
3096 3208 . "'" . esc_js( $bk_hash ) . "',"
3097 3209 . "'" . esc_js( $custom_booking_form ) . "'"
3098 - . ' ); 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>';
3099 3218
3100 - $header_title .= '<a class=\'button button-secondary\'
3101 - title=\'' . esc_attr( str_replace( "'", '', __( 'Edit', 'booking' ) ) ) . '\'
3102 - href=\'' . esc_url( $edit_booking_url ) . '\' onclick=\'' . esc_attr( $edit_booking_onclick ) . '\' ><i class=\'wpbc_icn_draw\'></i></a>';
3103 3219
3104 -
3105 3220 $header_title .= '<span class=\'wpbc-buttons-separator\'></span>';
3106 3221 }
3107 3222 // Trash
3108 3223 //$header_title .= '<a class=\'button button-secondary\' href=\'javascript:;\' onclick=\'javascript:delete_booking(' . $bk_id . ', ' . $this->current_user_id . ', &quot;' . wpbc_get_maybe_reloaded_booking_locale() . '&quot; , 1 );\' ><i class=\'wpbc_icn_delete_outline\'></i></a>';
@@ -3378,9 +3493,17 @@
3378 3493 $clean_key = sanitize_key( wp_unslash( (string) $tl_key ) );
3379 3494 if ( '' === $clean_key || ! isset( $allowed_timeline_keys[ $clean_key ] ) ) {
3380 3495 continue;
3381 3496 }
3382 - $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 );
3383 3506 }
3384 3507
3385 3508 // phpcs:ignore WordPress.Security.NonceVerification.Missing
3386 3509 if ( isset( $_POST['nav_step'] ) && ! is_scalar( $_POST['nav_step'] ) ) {
@@ -3387,11 +3510,16 @@
3387 3510 status_header( 400 );
3388 3511 wp_die( '' );
3389 3512 }
3390 3513
3391 - $attr['nav_step'] = isset( $_POST['nav_step'] ) ? wpbc_clean_text_value( wp_unslash( (string) $_POST['nav_step'] ) ) : '0'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
3392 - $attr['is_frontend'] = isset( $attr['is_frontend'] ) ? $attr['is_frontend'] : '1';
3393 - 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'] ) {
3394 3522 status_header( 400 );
3395 3523 wp_die( '' );
3396 3524 }
3397 3525