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 +139 -14 11.8.2 → 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
@@ -3381,9 +3493,17 @@
3381 3493 $clean_key = sanitize_key( wp_unslash( (string) $tl_key ) );
3382 3494 if ( '' === $clean_key || ! isset( $allowed_timeline_keys[ $clean_key ] ) ) {
3383 3495 continue;
3384 3496 }
3385 - $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 );
3386 3506 }
3387 3507
3388 3508 // phpcs:ignore WordPress.Security.NonceVerification.Missing
3389 3509 if ( isset( $_POST['nav_step'] ) && ! is_scalar( $_POST['nav_step'] ) ) {
@@ -3390,11 +3510,16 @@
3390 3510 status_header( 400 );
3391 3511 wp_die( '' );
3392 3512 }
3393 3513
3394 - $attr['nav_step'] = isset( $_POST['nav_step'] ) ? wpbc_clean_text_value( wp_unslash( (string) $_POST['nav_step'] ) ) : '0'; // phpcs:ignore WordPress.Security.NonceVerification.Missing
3395 - $attr['is_frontend'] = isset( $attr['is_frontend'] ) ? $attr['is_frontend'] : '1';
3396 - 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'] ) {
3397 3522 status_header( 400 );
3398 3523 wp_die( '' );
3399 3524 }
3400 3525