<?php

        if ( is_null( $query ) ) {
            return;
        }

        /*
         * This is not meant to be foolproof -- but it will catch obviously incorrect usage.
         *
         * Note: str_contains() is not used here, as this file can be included
         * directly outside of WordPress core, e.g. by HyperDB, in which case
         * the polyfills from wp-includes/compat.php are not loaded.
         */
        if ( false === strpos( $query, '%' ) ) {
            wp_load_translations_early();
            _doing_it_wrong(
                'wpdb::prepare',
                sprintf(
                    /* translators: %s: wpdb::prepare() */
                    __( 'The query argument of %s must have a placeholder.' ),
                    'wpdb::prepare()'
                ),
                '3.9.0'
            );
        }

        /*
         * Specify the formatting allowed in a placeholder. The following are allowed:
         *
         * - Sign specifier, e.g. $+d
         * - Numbered placeholders, e.g. %1$s
         * - Padding specifier, including custom padding characters, e.g. %05s, %'#5s
         * - Alignment specifier, e.g. %05-s
         * - Precision specifier, e.g. %.2f
         */
        $allowed_format = '(?:[1-9][0-9]*[$])?[-+0-9]*(?: |0|\'.)?[-+0-9]*(?:\.[0-9]+)?';

        /*
         * If a %s placeholder already has quotes around it, removing the existing quotes
         * and re-inserting them ensures the quotes are consistent.
         *
         * For backward compatibility, this is only applied to %s, and not to placeholders like %1$s,
         * which are frequently used in the middle of longer strings, or as table name placeholders.
         */
        $query = str_replace( "'%s'", '%s', $query ); // Strip any existing single quotes.
        $query = str_replace( '"%s"', '%s', $query ); // Strip any existing double quotes.

        // Escape any unescaped percents (i.e. anything unrecognised).
        $query = preg_replace( "/%(?:%|$|(?!(\.\.\.)?($allowed_format)?[sdfFi]))/", '%%\\1', $query );

        // Extract placeholders from the query.
        $split_query = preg_split( "/(^|[^%]|(?:%%)+)(%(?:\.\.\.)?(?:$allowed_format)?[sdfFi])/", $query, -1, PREG_SPLIT_DELIM_CAPTURE );

        $split_query_count = count( $split_query );

        /*
         * Split always returns with 1 value before the first placeholder (even with $query = "%s"),
         * then 3 additional values per placeholder.
         */
        $placeholder_count = ( ( $split_query_count - 1 ) / 3 );

        // If args were passed as an array, as in vsprintf(), move them up.
        $passed_as_array = ( isset( $args[0] ) && is_array( $args[0] ) && 1 === count( $args ) );
        if ( $passed_as_array && isset( $split_query[2] ) && substr( $split_query[2], 1, 3 ) === '...' && isset( $args[0][0] ) && false === is_array( $args[0][0] ) ) {
            $passed_as_array = false; // The first (and only) placeholder is using variadics (e.g. '%...d'), and that array has *not* been $passed_as_array, e.g. `$wpdb->prepare('id IN (%...d)', [ 1, 2, 3 ] );`.
        }
        if ( $passed_as_array ) {
            $args = $args[0];
        }

        $new_query       = '';
        $key             = 2; // Keys 0 and 1 in $split_query contain values before the first placeholder.
        $arg_current     = 0;
        $arg_offset      = 0;
        $arg_identifiers = array();
        $arg_strings     = array();
        $arg_variadics   = array();

        while ( $key < $split_query_count ) {

            // Glue (-2), any leading characters (-1); then the placeholder.
            $prefix      = $split_query[ $key - 2 ] . $split_query[ $key - 1 ];
            $placeholder = $split_query[ $key ];

            $variadic = ( '...' === substr( $placeholder, 1, 3 ) );
            if ( $variadic ) {
                $placeholder = '%' . substr( $placeholder, 4 );
            }

            $format = substr( $placeholder, 1, -1 );
            $type   = substr( $placeholder, -1 );

            $escaped = null;
            for ( $l = ( strlen( $prefix ) - 1 ); $l >= 0; $l-- ) {
                if ( '%' === $prefix[ $l ] ) {
                    $escaped = ( null === $escaped ? true : ! $escaped );
                } else {
                    break;
                }
            }

            if ( 'f' === $type && true === $this->allow_unsafe_unquoted_parameters && null !== $escaped ) {
                /*
                 * Before WP 6.2 the "force floats to be locale-unaware" RegEx didn't
                 * convert "%%%f" to "%%%F" (note the uppercase F).
                 * This was because it didn't check to see if the leading "%" was escaped.
                 * And because the "Escape any unescaped percents" RegEx used "[sdF]" in its
                 * negative lookahead assertion, when there was an odd number of "%", it added
                 * an extra "%", to give the fully escaped "%%%%f" (so it's never a placeholder).
                 */

                $new_placeholder = '%' . ( true === $escaped ? '' : '%' ) . $format . $type;

                --$placeholder_count;

            } elseif ( true === $escaped ) { // Don't change the $placeholder to contain an argnum.

                if ( $variadic ) {
                    $new_placeholder = '%...' . substr( $placeholder, 1 );
                } else {
                    $new_placeholder = $placeholder;
                }

                --$placeholder_count;

            } else {

                // Force floats to be locale-unaware.
                if ( 'f' === $type ) {
                    $type = 'F';
                }

                $set_format = ( '' !== $format );

                $argnum_pos = strpos( $format, '$' );
                if ( false !== $argnum_pos ) {
                    $argnum_value = (int) substr( $format, 0, $argnum_pos );
                    $format       = substr( $format, ( $argnum_pos + 1 ) );
                } else {
                    $argnum_value = ++$arg_current; // Argnum starts at 1.
                }

                $new_argnum = ( $argnum_value + $arg_offset );

                if ( $variadic ) {
                    if ( 'i' === $type ) {
                        $new_prefix        = '`%';
                        $new_suffix        = $format . 's`';
                        $arg_identifiers[] = ( $argnum_value - 1 );
                    } elseif ( 'd' === $type || 'F' === $type ) {
                        $new_prefix = '%'; // No need to quote integers or floats.
                        $new_suffix = $format . $type;
                    } else {
                        $new_prefix = "'%";
                        $new_suffix = $format . $type . "'";
                    }
                    $new_placeholder = '';
                    $arg_count       = count( $args[ ( $argnum_value - 1 ) ] ); // The argnum in $format starts from 1, but $args index start from 0.
                    for ( $k = 0; $k < $arg_count; $k++ ) {
                        $new_placeholder .= $new_prefix . ( $new_argnum + $k ) . '$' . $new_suffix . ',';
                    }
                    $new_placeholder = substr( $new_placeholder, 0, -1 );
                    $arg_offset     += ( $arg_count - 1 ); // The arg already counts as 1, the offset is how many extra to move.
                    $arg_variadics[] = ( $argnum_value - 1 );
                } elseif ( 'i' === $type ) {
                    $new_placeholder   = '`%' . $new_argnum . '$' . $format . 's`';
                    $arg_identifiers[] = ( $argnum_value - 1 );
                } elseif ( 'd' === $type || 'F' === $type ) {
                    $new_placeholder = '%' . $new_argnum . '$' . $format . $type; // No need to quote integers or floats.
                } else { // i.e. ( 's' === $type ).
                    if ( true === $this->allow_unsafe_unquoted_parameters && ( $set_format || null !== $escaped ) ) {
                            // Unquoted strings for backward compatibility (dangerous).
                            // First, "numbered or formatted string placeholders (eg, %1$s, %5s)"
                            // Second, if "%s" has a "%" before it, even if it's unrelated (e.g. "LIKE '%%%s%%'").
                        $new_placeholder = '%' . $new_argnum . '$' . $format . 's';
                    } else {
                        $new_placeholder = "'%" . $new_argnum . '$' . $format . "s'";
                    }
                    $arg_strings[] = ( $argnum_value - 1 );
                }
            }

            // Glue (-2), any leading characters (-1), then $new_placeholder.
            $new_query .= $prefix . $new_placeholder;

            $key += 3;

        }

        // Replace $query; and add remaining $query characters, or index 0 if there were no placeholders.
        $query = $new_query . $split_query[ $key - 2 ];

        $dual_use = array_intersect( $arg_identifiers, $arg_strings );

        if ( count( $dual_use ) > 0 ) {
            wp_load_translations_early();

            $used_placeholders = array();

            $key    = 2;
            $arg_id = 0;
            // Parse again (only used when there is an error).
            while ( $key < $split_query_count ) {
                $placeholder = $split_query[ $key ];

                $format = substr( $placeholder, 1, -1 );

                $argnum_pos = strpos( $format, '$' );

                if ( false !== $argnum_pos ) {
                    $arg_pos = ( ( (int) substr( $format, 0, $argnum_pos ) ) - 1 );
                } else {
                    $arg_pos = $arg_id;
                }

                $used_placeholders[ $arg_pos ][] = $placeholder;

                $key += 3;
                $arg_id++;
            }

            $conflicts = array();
            foreach ( $dual_use as $arg_pos ) {
                $conflicts[] = implode( ' and ', $used_placeholders[ $arg_pos ] );
            }

            _doing_it_wrong(
                'wpdb::prepare',
                sprintf(
                    /* translators: %s: A list of placeholders found to be a problem. */
                    __( 'Arguments cannot be prepared as both an Identifier and Value. Found the following conflicts: %s' ),
                    implode( ', ', $conflicts )
                ),
                '6.2.0'
            );

            return;
        }

        $args_count = count( $args );

        if ( $args_count !== $placeholder_count ) {
            if ( 1 === $placeholder_count && $passed_as_array ) {
                /*
                 * If the passed query only expected one argument,
                 * but the wrong number of arguments was sent as an array, bail.
                 */
                wp_load_translations_early();
                _doing_it_wrong(
                    'wpdb::prepare',
                    __( 'The query only expected one placeholder, but an array of multiple placeholders was sent.' ),
                    '4.9.0'
                );

                return;
            } else {
                /*
                 * If we don't have the right number of placeholders,
                 * but they were passed as individual arguments,
                 * or we were expecting multiple arguments in an array, throw a warning.
                 */
                wp_load_translations_early();
                _doing_it_wrong(
                    'wpdb::prepare',
                    sprintf(
                        /* translators: 1: Number of placeholders, 2: Number of arguments passed. */
                        __( 'The query does not contain the correct number of placeholders (%1$d) for the number of arguments passed (%2$d).' ),
                        $placeholder_count,
                        $args_count
                    ),
                    '4.8.3'
                );

                /*
                 * If we don't have enough arguments to match the placeholders,
                 * return an empty string to avoid a fatal error on PHP 8.
                 */
                if ( $args_count < $placeholder_count ) {
                    $max_numbered_placeholder = 0;

                    for ( $i = 2, $l = $split_query_count; $i < $l; $i += 3 ) {
                        // Assume a leading number is for a numbered placeholder, e.g. '%3$s'.
                        $argnum = (int) substr( $split_query[ $i ], 1 );

                        if ( $max_numbered_placeholder < $argnum ) {
                            $max_numbered_placeholder = $argnum;
                        }
                    }

                    if ( ! $max_numbered_placeholder || $args_count < $max_numbered_placeholder ) {
                        return '';
                    }
                }
            }
        }

        $args_escaped = array();

        foreach ( $args as $i => $value ) {
            if ( in_array( $i, $arg_variadics, true ) ) {
                if ( in_array( $i, $arg_identifiers, true ) ) {
                    $args_escaped = array_merge( $args_escaped, array_map( array( $this, '_escape_identifier_value' ), $value ) );
                } else {
                    $args_escaped = array_merge( $args_escaped, array_map( array( $this, '_real_escape' ), $value ) );
                }
            } elseif ( in_array( $i, $arg_identifiers, true ) ) {
                $args_escaped[] = $this->_escape_identifier_value( $value );
            } elseif ( is_int( $value ) || is_float( $value ) ) {
                $args_escaped[] = $value;
            } else {
                if ( ! is_scalar( $value ) && ! is_null( $value ) ) {
                    wp_load_translations_early();
                    _doing_it_wrong(
                        'wpdb::prepare',
                        sprintf(
                            /* translators: %s: Value type. */
                            __( 'Unsupported value type (%s).' ),
                            gettype( $value )
                        ),
                        '4.8.2'
                    );

                    // Preserving old behavior, where values are escaped as strings.
                    $value = '';
                }

                $args_escaped[] = $this->_real_escape( $value );
            }
        }

        $query = vsprintf( $query, $args_escaped );

        return $this->add_placeholder_escape( $query );