idate_date_values().
 *
 * @link https://developer.wordpress.org/reference/classes/wp_query/
 *
 * @since 3.7.0
 */
#[AllowDynamicProperties]
class WP_Date_Query {
	/**
	 * Array of date queries.
	 *
	 * See WP_Date_Query::__construct() for information on date query arguments.
	 *
	 * @since 3.7.0
	 * @var array
	 */
	public $queries = array();

	/**
	 * The default relation between top-level queries. Can be either 'AND' or 'OR'.
	 *
	 * @since 3.7.0
	 * @var string
	 */
	public $relation = 'AND';

	/**
	 * The column to query against. Can be changed via the query arguments.
	 *
	 * @since 3.7.0
	 * @var string
	 */
	public $column = 'post_date';

	/**
	 * The value comparison operator. Can be changed via the query arguments.
	 *
	 * @since 3.7.0
	 * @var string
	 */
	public $compare = '=';

	/**
	 * Supported time-related parameter keys.
	 *
	 * @since 4.1.0
	 * @var string[]
	 */
	public $time_keys = array( 'after', 'before', 'year', 'month', 'monthnum', 'week', 'w', 'dayofyear', 'day', 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second' );

	/**
	 * Constructor.
	 *
	 * Time-related parameters that normally require integer values ('year', 'month', 'week', 'dayofyear', 'day',
	 * 'dayofweek', 'dayofweek_iso', 'hour', 'minute', 'second') accept arrays of integers for some values of
	 * 'compare'. When 'compare' is 'IN' or 'NOT IN', arrays are accepted; when 'compare' is 'BETWEEN' or 'NOT
	 * BETWEEN', arrays of two valid values are required. See individual argument descriptions for accepted values.
	 *
	 * @since 3.7.0
	 * @since 4.0.0 The $inclusive logic was updated to include all times within the date range.
	 * @since 4.1.0 Introduced 'dayofweek_iso' time type parameter.
	 *
	 * @param array  $date_query {
	 *     Array of date query clauses.
	 *
	 *     @type array ...$0 {
	 *         @type string $column   Optional. The column to query against. If undefined, inherits the value of
	 *                                the `$default_column` parameter. See WP_Date_Query::validate_column() and
	 *                                the {@see 'date_query_valid_columns'} filter for the list of accepted values.
	 *                                Default 'post_date'.
	 *         @type string $compare  Optional. The comparison operator. Accepts '=', '!=', '>', '>=', '<', '<=',
	 *                                'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. Default '='.
	 *         @type string $relation Optional. The boolean relationship between the date queries. Accepts 'OR' or 'AND'.
	 *                                Default 'OR'.
	 *         @type array  ...$0 {
	 *             Optional. An array of first-order clause parameters, or another fully-formed date query.
	 *
	 *             @type string|array $before {
	 *                 Optional. Date to retrieve posts before. Accepts `strtotime()`-compatible string,
	 *                 or array of 'year', 'month', 'day' values.
	 *
	 *                 @type string $year  The four-digit year. Default empty. Accepts any four-digit year.
	 *                 @type string $month Optional when passing array.The month of the year.
	 *                                     Default (string:empty)|(array:1). Accepts numbers 1-12.
	 *                 @type string $day   Optional when passing array.The day of the month.
	 *                                     Default (string:empty)|(array:1). Accepts numbers 1-31.
	 *             }
	 *             @type string|array $after {
	 *                 Optional. Date to retrieve posts after. Accepts `strtotime()`-compatible string,
	 *                 or array of 'year', 'month', 'day' values.
	 *
	 *                 @type string $year  The four-digit year. Accepts any four-digit year. Default empty.
	 *                 @type string $month Optional when passing array. The month of the year. Accepts numbers 1-12.
	 *                                     Default (string:empty)|(array:12).
	 *                 @type string $day   Optional when passing array.The day of the month. Accepts numbers 1-31.
	 *                                     Default (string:empty)|(array:last day of month).
	 *             }
	 *             @type string       $column        Optional. Used to add a clause comparing a column other than
	 *                                               the column specified in the top-level `$column` parameter.
	 *                                               See WP_Date_Query::validate_column() and
	 *                                               the {@see 'date_query_valid_columns'} filter for the list
	 *                                               of accepted values. Default is the value of top-level `$column`.
	 *             @type string       $compare       Optional. The comparison operator. Accepts '=', '!=', '>', '>=',
	 *                                               '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN'. 'IN',
	 *                                               'NOT IN', 'BETWEEN', and 'NOT BETWEEN'. Comparisons support
	 *                                               arrays in some time-related parameters. Default '='.
	 *             @type bool         $inclusive     Optional. Include results from dates specified in 'before' or
	 *                                               'after'. Default false.
	 *             @type int|int[]    $year          Optional. The four-digit year number. Accepts any four-digit year
	 *                                               or an array of years if `$compare` supports it. Default empty.
	 *             @type int|int[]    $month         Optional. The two-digit month number. Accepts numbers 1-12 or an
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
	 *             @type int|int[]    $week          Optional. The week number of the year. Accepts numbers 0-53 or an
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
	 *             @type int|int[]    $dayofyear     Optional. The day number of the year. Accepts numbers 1-366 or an
	 *                                               array of valid numbers if `$compare` supports it.
	 *             @type int|int[]    $day           Optional. The day of the month. Accepts numbers 1-31 or an array
	 *                                               of valid numbers if `$compare` supports it. Default empty.
	 *             @type int|int[]    $dayofweek     Optional. The day number of the week. Accepts numbers 1-7 (1 is
	 *                                               Sunday) or an array of valid numbers if `$compare` supports it.
	 *                                               Default empty.
	 *             @type int|int[]    $dayofweek_iso Optional. The day number of the week (ISO). Accepts numbers 1-7
	 *                                               (1 is Monday) or an array of valid numbers if `$compare` supports it.
	 *                                               Default empty.
	 *             @type int|int[]    $hour          Optional. The hour of the day. Accepts numbers 0-23 or an array
	 *                                               of valid numbers if `$compare` supports it. Default empty.
	 *             @type int|int[]    $minute        Optional. The minute of the hour. Accepts numbers 0-59 or an array
	 *                                               of valid numbers if `$compare` supports it. Default empty.
	 *             @type int|int[]    $second        Optional. The second of the minute. Accepts numbers 0-59 or an
	 *                                               array of valid numbers if `$compare` supports it. Default empty.
	 *         }
	 *     }
	 * }
	 * @param string $default_column Optional. Default column to query against. See WP_Date_Query::validate_column()
	 *                               and the {@see 'date_query_valid_columns'} filter for the list of accepted values.
	 *                               Default 'post_date'.
	 */
	public function __construct( $date_query, $default_column = 'post_date' ) {
		if ( empty( $date_query ) || ! is_array( $date_query ) ) {
			return;
		}

		if ( isset( $date_query['relation'] ) ) {
			$this->relation = $this->sanitize_relation( $date_query['relation'] );
		} else {
			$this->relation = 'AND';
		}

		// Support for passing time-based keys in the top level of the $date_query array.
		if ( ! isset( $date_query[0] ) ) {
			$date_query = array( $date_query );
		}

		if ( ! empty( $date_query['column'] ) ) {
			$date_query['column'] = esc_sql( $date_query['column'] );
		} else {
			$date_query['column'] = esc_sql( $default_column );
		}

		$this->column = $this->validate_column( $this->column );

		$this->compare = $this->get_compare( $date_query );

		$this->queries = $this->sanitize_query( $date_query );
	}

	/**
	 * Recursive-friendly query sanitizer.
	 *
	 * Ensures that each query-level clause has a 'relation' key, and that
	 * each first-order clause contains all the necessary keys from `$defaults`.
	 *
	 * @since 4.1.0
	 *
	 * @param array $queries
	 * @param array $parent_query
	 * @return array Sanitized queries.
	 */
	public function sanitize_query( $queries, $parent_query = null ) {
		$cleaned_query = array();

		$defaults = array(
			'column'   => 'post_date',
			'compare'  => '=',
			'relation' => 'AND',
		);

		// Numeric keys should always have array values.
		foreach ( $queries as $qkey => $qvalue ) {
			if ( is_numeric( $qkey ) && ! is_array( $qvalue ) ) {
				unset( $queries[ $qkey ] );
			}
		}

		// Each query should have a value for each default key. Inherit from the parent when possible.
		foreach ( $defaults as $dkey => $dvalue ) {
			if ( isset( $queries[ $dkey ] ) ) {
				continue;
			}

			if ( isset( $parent_query[ $dkey ] ) ) {
				$queries[ $dkey ] = $parent_query[ $dkey ];
			} else {
				$queries[ $dkey ] = $dvalue;
			}
		}

		// Validate the dates passed in the query.
		if ( $this->is_first_order_clause( $queries ) ) {
			$this->validate_date_values( $queries );
		}

		// Sanitize the relation parameter.
		$queries['relation'] = $this->sanitize_relation( $queries['relation'] );

		foreach ( $queries as $key => $q ) {
			if ( ! is_array( $q ) || in_array( $key, $this->time_keys, true ) ) {
				// This is a first-order query. Trust the values and sanitize when building SQL.
				$cleaned_query[ $key ] = $q;
			} else {
				// Any array without a time key is another query, so we recurse.
				$cleaned_query[] = $this->sanitize_query( $q, $queries );
			}
		}

		return $cleaned_query;
	}

	/**
	 * Determines whether this is a first-order clause.
	 *
	 * Checks to see if the current clause has any time-related keys.
	 * If so, it's first-order.
	 *
	 * @since 4.1.0
	 *
	 * @param array $query Query clause.
	 * @return bool True if this is a first-order clause.
	 */
	protected function is_first_order_clause( $query ) {
		$time_keys = array_intersect( $this->time_keys, array_keys( $query ) );
		return ! empty( $time_keys );
	}

	/**
	 * Determines and validates what comparison operator to use.
	 *
	 * @since 3.7.0
	 *
	 * @param array $query A date query or a date subquery.
	 * @return string The comparison operator.
	 */
	public function get_compare( $query ) {
		if ( ! empty( $query['compare'] )
			&& in_array( $query['compare'], array( '=', '!=', '>', '>=', '<', '<=', 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true )
		) {
			return strtoupper( $query['compare'] );
		}

		return $this->compare;
	}

	/**
	 * Validates the given date_query values and triggers errors if something is not valid.
	 *
	 * Note that date queries with invalid date ranges are allowed to
	 * continue (though of course no items will be found for impossible dates).
	 * This method only generates debug notices for these cases.
	 *
	 * @since 4.1.0
	 *
	 * @param array $date_query The date_query array.
	 * @return bool True if all values in the query are valid, false if one or more fail.
	 */
	public function validate_date_values( $date_query = array() ) {
		if ( empty( $date_query ) ) {
			return false;
		}

		$valid = true;

		/*
		 * Validate 'before' and 'after' up front, then let the
		 * validation routine continue to be sure that all invalid
		 * values generate errors too.
		 */
		if ( array_key_exists( 'before', $date_query ) && is_array( $date_query['before'] ) ) {
			$valid = $this->validate_date_values( $date_query['before'] );
		}

		if ( array_key_exists( 'after', $date_query ) && is_array( $date_query['after'] ) ) {
			$valid = $this->validate_date_values( $date_query['after'] );
		}

		// Array containing all min-max checks.
		$min_max_checks = array();

		// Days per year.
		if ( array_key_exists( 'year', $date_query ) ) {
			/*
			 * If a year exists in the date query, we can use it to get the days.
			 * If multiple years are provided (as in a BETWEEN), use the first one.
			 */
			if ( is_array( $date_query['year'] ) ) {
				$_year = reset( $date_query['year'] );
			} else {
				$_year = $date_query['year'];
			}

			$max_days_of_year = (int) gmdate( 'z', mktime( 0, 0, 0, 12, 31, $_year ) ) + 1;
		} else {
			// Otherwise we use the max of 366 (leap-year).
			$max_days_of_year = 366;
		}

		$min_max_checks['dayofyear'] = array(
			'min' => 1,
			'max' => $max_days_of_year,
		);

		// Days per week.
		$min_max_checks['dayofweek'] = array(
			'min' => 1,
			'max' => 7,
		);

		// Days per week.
		$min_max_checks['dayofweek_iso'] = array(
			'min' => 1,
			'max' => 7,
		);

		// Months per year.
		$min_max_checks['month'] = array(
			'min' => 1,
			'max' => 12,
		);

		// Weeks per year.
		if ( isset( $_year ) ) {
			/*
			 * If we have a specific year, use it to calculate number of weeks.
			 * Note: the number of weeks in a year is the date in which Dec 28 appears.
			 */
			$week_count = gmdate( 'W', mktime( 0, 0, 0, 12, 28, $_year ) );

		} else {
			// Otherwise set the week-count to a maximum of 53.
			$week_count = 53;
		}

		$min_max_checks['week'] = array(
			'min' => 1,
			'max' => $week_count,
		);

		// Days per month.
		$min_max_checks['day'] = array(
			'min' => 1,
			'max' => 31,
		);

		// Hours per day.
		$min_max_checks['hour'] = array(
			'min' => 0,
			'max' => 23,
		);

		// Minutes per hour.
		$min_max_checks['minute'] = array(
			'min' => 0,
			'max' => 59,
		);

		// Seconds per minute.
		$min_max_checks['second'] = array(
			'min' => 0,
			'max' => 59,
		);

		// Concatenate and throw a notice for each invalid value.
		foreach ( $min_max_checks as $key => $check ) {
			if ( ! array_key_exists( $key, $date_query ) ) {
				continue;
			}

			// Throw a notice for each failing value.
			foreach ( (array) $date_query[ $key ] as $_value ) {
				$is_between = $_value >= $check['min'] && $_value <= $check['max'];

				if ( ! is_numeric( $_value ) || ! $is_between ) {
					$error = sprintf(
						/* translators: Date query invalid date message. 1: Invalid value, 2: Type of value, 3: Minimum valid value, 4: Maximum valid value. */
						__( 'Invalid value %1$s for %2$s. Expected value should be between %3$s and %4$s.' ),
						'<code>' . esc_html( $_value ) . '</code>',
						'<code>' . esc_html( $key ) . '</code>',
						'<code>' . esc_html( $check['min'] ) . '</code>',
						'<code>' . esc_html( $check['max'] ) . '</code>'
					);

					_doing_it_wrong( __CLASS__, $error, '4.1.0' );

					$valid = false;
				}
			}
		}

		// If we already have invalid date messages, don't bother running through checkdate().
		if ( ! $valid ) {
			return $valid;
		}

		$day_month_year_error_msg = '';

		$day_exists   = array_key_exists( 'day', $date_query ) && is_numeric( $date_query['day'] );
		$month_exists = array_key_exists( 'month', $date_query ) && is_numeric( $date_query['month'] );
		$year_exists  = array_key_exists( 'year', $date_query ) && is_numeric( $date_query['year'] );

		if ( $day_exists && $month_exists && $year_exists ) {
			// 1. Checking day, month, year combination.
			if ( ! wp_checkdate( $date_query['month'], $date_query['day'], $date_query['year'], sprintf( '%s-%s-%s', $date_query['year'], $date_query['month'], $date_query['day'] ) ) ) {
				$day_month_year_error_msg = sprintf(
					/* translators: 1: Year, 2: Month, 3: Day of month. */
					__( 'The following values do not describe a valid date: year %1$s, month %2$s, day %3$s.' ),
					'<code>' . esc_html( $date_query['year'] ) . '</code>',
					'<code>' . esc_html( $date_query['month'] ) . '</code>',
					'<code>' . esc_html( $date_query['day'] ) . '</code>'
				);

				$valid = false;
			}
		} elseif ( $day_exists && $month_exists ) {
			/*
			 * 2. checking day, month combination
			 * We use 2012 because, as a leap year, it's the most permissive.
			 */
			if ( ! wp_checkdate( $date_query['month'], $date_query['day'], 2012, sprintf( '2012-%s-%s', $date_query['month'], $date_query['day'] ) ) ) {
				$day_month_year_error_msg = sprintf(
					/* translators: 1: Month, 2: Day of month. */
					__( 'The following values do not describe a valid date: month %1$s, day %2$s.' ),
					'<code>' . esc_html( $date_query['month'] ) . '</code>',
					'<code>' . esc_html( $date_query['day'] ) . '</code>'
				);

				$valid = false;
			}
		}

		if ( ! empty( $day_month_year_error_msg ) ) {
			_doing_it_wrong( __CLASS__, $day_month_year_error_msg, '4.1.0' );
		}

		return $valid;
	}

	/**
	 * Validates a column name parameter.
	 *
	 * Column names without a table prefix (like 'post_date') are checked against a list of
	 * allowed and known tables, and then, if found, have a table prefix (such as 'wp_posts.')
	 * prepended. Prefixed column names (such as 'wp_posts.post_date') bypass this allowed
	 * check, and are only sanitized to remove illegal characters.
	 *
	 * @since 3.7.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $column The user-supplied column name.
	 * @return string A validated column name value.
	 */
	public function validate_column( $column ) {
		global $wpdb;

		$valid_columns = array(
			'post_date',         // Part of $wpdb->posts.
			'post_date_gmt',     // Part of $wpdb->posts.
			'post_modified',     // Part of $wpdb->posts.
			'post_modified_gmt', // Part of $wpdb->posts.
			'comment_date',      // Part of $wpdb->comments.
			'comment_date_gmt',  // Part of $wpdb->comments.
			'user_registered',   // Part of $wpdb->users.
		);

		if ( is_multisite() ) {
			$valid_columns = array_merge(
				$valid_columns,
				array(
					'registered',   // Part of $wpdb->blogs.
					'last_updated', // Part of $wpdb->blogs.
				)
			);
		}

		// Attempt to detect a table prefix.
		if ( ! str_contains( $column, '.' ) ) {
			/**
			 * Filters the list of valid date query columns.
			 *
			 * @since 3.7.0
			 * @since 4.1.0 Added 'user_registered' to the default recognized columns.
			 * @since 4.6.0 Added 'registered' and 'last_updated' to the default recognized columns.
			 *
			 * @param string[] $valid_columns An array of valid date query columns. Defaults
			 *                                are 'post_date', 'post_date_gmt', 'post_modified',
			 *                                'post_modified_gmt', 'comment_date', 'comment_date_gmt',
			 *                                'user_registered', 'registered', 'last_updated'.
			 */
			if ( ! in_array( $column, apply_filters( 'date_query_valid_columns', $valid_columns ), true ) ) {
				$column = 'post_date';
			}

			$known_columns = array(
				$wpdb->posts    => array(
					'post_date',
					'post_date_gmt',
					'post_modified',
					'post_modified_gmt',
				),
				$wpdb->comments => array(
					'comment_date',
					'comment_date_gmt',
				),
				$wpdb->users    => array(
					'user_registered',
				),
			);

			if ( is_multisite() ) {
				$known_columns[ $wpdb->blogs ] = array(
					'registered',
					'last_updated',
				);
			}

			// If it's a known column name, add the appropriate table prefix.
			foreach ( $known_columns as $table_name => $table_columns ) {
				if ( in_array( $column, $table_columns, true ) ) {
					$column = $table_name . '.' . $column;
					break;
				}
			}
		}

		// Remove unsafe characters.
		return preg_replace( '/[^a-zA-Z0-9_$\.]/', '', $column );
	}

	/**
	 * Generates WHERE clause to be appended to a main query.
	 *
	 * @since 3.7.0
	 *
	 * @return string MySQL WHERE clause.
	 */
	public function get_sql() {
		$sql = $this->get_sql_clauses();

		$where = $sql['where'];

		/**
		 * Filters the date query WHERE clause.
		 *
		 * @since 3.7.0
		 *
		 * @param string        $where WHERE clause of the date query.
		 * @param WP_Date_Query $query The WP_Date_Query instance.
		 */
		return apply_filters( 'get_date_sql', $where, $this );
	}

	/**
	 * Generates SQL clauses to be appended to a main query.
	 *
	 * Called by the public WP_Date_Query::get_sql(), this method is abstracted
	 * out to maintain parity with the other Query classes.
	 *
	 * @since 4.1.0
	 *
	 * @return string[] {
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
	 *
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
	 *     @type string $where SQL fragment to append to the main WHERE clause.
	 * }
	 */
	protected function get_sql_clauses() {
		$sql = $this->get_sql_for_query( $this->queries );

		if ( ! empty( $sql['where'] ) ) {
			$sql['where'] = ' AND ' . $sql['where'];
		}

		return $sql;
	}

	/**
	 * Generates SQL clauses for a single query array.
	 *
	 * If nested subqueries are found, this method recurses the tree to
	 * produce the properly nested SQL.
	 *
	 * @since 4.1.0
	 *
	 * @param array $query Query to parse.
	 * @param int   $depth Optional. Number of tree levels deep we currently are.
	 *                     Used to calculate indentation. Default 0.
	 * @return array {
	 *     Array containing JOIN and WHERE SQL clauses to append to a single query array.
	 *
	 *     @type string $join  SQL fragment to append to the main JOIN clause.
	 *     @type string $where SQL fragment to append to the main WHERE clause.
	 * }
	 */
	protected function get_sql_for_query( $query, $depth = 0 ) {
		$sql_chunks = array(
			'join'  => array(),
			'where' => array(),
		);

		$sql = array(
			'join'  => '',
			'where' => '',
		);

		$indent = '';
		for ( $i = 0; $i < $depth; $i++ ) {
			$indent .= '  ';
		}

		foreach ( $query as $key => $clause ) {
			if ( 'relation' === $key ) {
				$relation = $query['relation'];
			} elseif ( is_array( $clause ) ) {

				// This is a first-order clause.
				if ( $this->is_first_order_clause( $clause ) ) {
					$clause_sql = $this->get_sql_for_clause( $clause, $query );

					$where_count = count( $clause_sql['where'] );
					if ( ! $where_count ) {
						$sql_chunks['where'][] = '';
					} elseif ( 1 === $where_count ) {
						$sql_chunks['where'][] = $clause_sql['where'][0];
					} else {
						$sql_chunks['where'][] = '( ' . implode( ' AND ', $clause_sql['where'] ) . ' )';
					}

					$sql_chunks['join'] = array_merge( $sql_chunks['join'], $clause_sql['join'] );
					// This is a subquery, so we recurse.
				} else {
					$clause_sql = $this->get_sql_for_query( $clause, $depth + 1 );

					$sql_chunks['where'][] = $clause_sql['where'];
					$sql_chunks['join'][]  = $clause_sql['join'];
				}
			}
		}

		// Filter to remove empties.
		$sql_chunks['join']  = array_filter( $sql_chunks['join'] );
		$sql_chunks['where'] = array_filter( $sql_chunks['where'] );

		if ( empty( $relation ) ) {
			$relation = 'AND';
		}

		// Filter duplicate JOIN clauses and combine into a single string.
		if ( ! empty( $sql_chunks['join'] ) ) {
			$sql['join'] = implode( ' ', array_unique( $sql_chunks['join'] ) );
		}

		// Generate a single WHERE clause with proper brackets and indentation.
		if ( ! empty( $sql_chunks['where'] ) ) {
			$sql['where'] = '( ' . "\n  " . $indent . implode( ' ' . "\n  " . $indent . $relation . ' ' . "\n  " . $indent, $sql_chunks['where'] ) . "\n" . $indent . ')';
		}

		return $sql;
	}

	/**
	 * Turns a single date clause into pieces for a WHERE clause.
	 *
	 * A wrapper for get_sql_for_clause(), included here for backward
	 * compatibility while retaining the naming convention across Query classes.
	 *
	 * @since 3.7.0
	 *
	 * @param array $query Date query arguments.
	 * @return array {
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
	 *
	 *     @type string[] $join  Array of SQL fragments to append to the main JOIN clause.
	 *     @type string[] $where Array of SQL fragments to append to the main WHERE clause.
	 * }
	 */
	protected function get_sql_for_subquery( $query ) {
		return $this->get_sql_for_clause( $query, '' );
	}

	/**
	 * Turns a first-order date query into SQL for a WHERE clause.
	 *
	 * @since 4.1.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param array $query        Date query clause.
	 * @param array $parent_query Parent query of the current date query.
	 * @return array {
	 *     Array containing JOIN and WHERE SQL clauses to append to the main query.
	 *
	 *     @type string[] $join  Array of SQL fragments to append to the main JOIN clause.
	 *     @type string[] $where Array of SQL fragments to append to the main WHERE clause.
	 * }
	 */
	protected function get_sql_for_clause( $query, $parent_query ) {
		global $wpdb;

		// The sub-parts of a $where part.
		$where_parts = array();

		$column = ( ! empty( $query['column'] ) ) ? esc_sql( $query['column'] ) : $this->column;

		$column = $this->validate_column( $column );

		$compare = $this->get_compare( $query );

		$inclusive = ! empty( $query['inclusive'] );

		// Assign greater- and less-than values.
		$lt = '<';
		$gt = '>';

		if ( $inclusive ) {
			$lt .= '=';
			$gt .= '=';
		}

		// Range queries.
		if ( ! empty( $query['after'] ) ) {
			$where_parts[] = $wpdb->prepare( "$column $gt %s", $this->build_mysql_datetime( $query['after'], ! $inclusive ) );
		}
		if ( ! empty( $query['before'] ) ) {
			$where_parts[] = $wpdb->prepare( "$column $lt %s", $this->build_mysql_datetime( $query['before'], $inclusive ) );
		}
		// Specific value queries.

		$date_units = array(
			'YEAR'           => array( 'year' ),
			'MONTH'          => array( 'month', 'monthnum' ),
			'_wp_mysql_week' => array( 'week', 'w' ),
			'DAYOFYEAR'      => array( 'dayofyear' ),
			'DAYOFMONTH'     => array( 'day' ),
			'DAYOFWEEK'      => array( 'dayofweek' ),
			'WEEKDAY'        => array( 'dayofweek_iso' ),
		);

		// Check of the possible date units and add them to the query.
		foreach ( $date_units as $sql_part => $query_parts ) {
			foreach ( $query_parts as $query_part ) {
				if ( isset( $query[ $query_part ] ) ) {
					$value = $this->build_value( $compare, $query[ $query_part ] );
					if ( $value ) {
						switch ( $sql_part ) {
							case '_wp_mysql_week':
								$where_parts[] = _wp_mysql_week( $column ) . " $compare $value";
								break;
							case 'WEEKDAY':
								$where_parts[] = "$sql_part( $column ) + 1 $compare $value";
								break;
							default:
								$where_parts[] = "$sql_part( $column ) $compare $value";
						}

						break;
					}
				}
			}
		}

		if ( isset( $query['hour'] ) || isset( $query['minute'] ) || isset( $query['second'] ) ) {
			// Avoid notices.
			foreach ( array( 'hour', 'minute', 'second' ) as $unit ) {
				if ( ! isset( $query[ $unit ] ) ) {
					$query[ $unit ] = null;
				}
			}

			$time_query = $this->build_time_query( $column, $compare, $query['hour'], $query['minute'], $query['second'] );
			if ( $time_query ) {
				$where_parts[] = $time_query;
			}
		}

		/*
		 * Return an array of 'join' and 'where' for compatibility
		 * with other query classes.
		 */
		return array(
			'where' => $where_parts,
			'join'  => array(),
		);
	}

	/**
	 * Builds and validates a value string based on the comparison operator.
	 *
	 * @since 3.7.0
	 *
	 * @param string       $compare The compare operator to use.
	 * @param string|array $value   The value.
	 * @return string|false|int The value to be used in SQL or false on error.
	 */
	public function build_value( $compare, $value ) {
		if ( ! isset( $value ) ) {
			return false;
		}

		switch ( $compare ) {
			case 'IN':
			case 'NOT IN':
				$value = (array) $value;

				// Remove non-numeric values.
				$value = array_filter( $value, 'is_numeric' );

				if ( empty( $value ) ) {
					return false;
				}

				return '(' . implode( ',', array_map( 'intval', $value ) ) . ')';

			case 'BETWEEN':
			case 'NOT BETWEEN':
				if ( ! is_array( $value ) || 2 !== count( $value ) ) {
					$value = array( $value, $value );
				} else {
					$value = array_values( $value );
				}

				// If either value is non-numeric, bail.
				foreach ( $value as $v ) {
					if ( ! is_numeric( $v ) ) {
						return false;
					}
				}

				$value = array_map( 'intval', $value );

				return $value[0] . ' AND ' . $value[1];

			default:
				if ( ! is_numeric( $value ) ) {
					return false;
				}

				return (int) $value;
		}
	}

	/**
	 * Builds a MySQL format date/time based on some query parameters.
	 *
	 * You can pass an array of values (year, month, etc.) with missing parameter values being defaulted to
	 * either the maximum or minimum values (controlled by the $default_to parameter). Alternatively you can
	 * pass a string that will be passed to date_create().
	 *
	 * @since 3.7.0
	 *
	 * @param string|array $datetime       An array of parameters or a strtotime() string.
	 * @param bool         $default_to_max Whether to round up incomplete dates. Supported by values
	 *                                     of $datetime that are arrays, or string values that are a
	 *                                     subset of MySQL date format ('Y', 'Y-m', 'Y-m-d', 'Y-m-d H:i').
	 *                                     Default: false.
	 * @return string|false A MySQL format date/time or false on failure.
	 */
	public function build_mysql_datetime( $datetime, $default_to_max = false ) {
		if ( ! is_array( $datetime ) ) {

			/*
			 * Try to parse some common date formats, so we can detect
			 * the level of precision and support the 'inclusive' parameter.
			 */
			if ( preg_match( '/^(\d{4})$/', $datetime, $matches ) ) {
				// Y
				$datetime = array(
					'year' => (int) $matches[1],
				);

			} elseif ( preg_match( '/^(\d{4})\-(\d{2})$/', $datetime, $matches ) ) {
				// Y-m
				$datetime = array(
					'year'  => (int) $matches[1],
					'month' => (int) $matches[2],
				);

			} elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2})$/', $datetime, $matches ) ) {
				// Y-m-d
				$datetime = array(
					'year'  => (int) $matches[1],
					'month' => (int) $matches[2],
					'day'   => (int) $matches[3],
				);

			} elseif ( preg_match( '/^(\d{4})\-(\d{2})\-(\d{2}) (\d{2}):(\d{2})$/', $datetime, $matches ) ) {
				// Y-m-d H:i
				$datetime = array(
					'year'   => (int) $matches[1],
					'month'  => (int) $matches[2],
					'day'    => (int) $matches[3],
					'hour'   => (int) $matches[4],
					'minute' => (int) $matches[5],
				);
			}

			// If no match is found, we don't support default_to_max.
			if ( ! is_array( $datetime ) ) {
				$wp_timezone = wp_timezone();

				// Assume local timezone if not provided.
				$dt = date_create( $datetime, $wp_timezone );

				if ( false === $dt ) {
					return gmdate( 'Y-m-d H:i:s', false );
				}

				return $dt->setTimezone( $wp_timezone )->format( 'Y-m-d H:i:s' );
			}
		}

		$datetime = array_map( 'absint', $datetime );

		if ( ! isset( $datetime['year'] ) ) {
			$datetime['year'] = current_time( 'Y' );
		}

		if ( ! isset( $datetime['month'] ) ) {
			$datetime['month'] = ( $default_to_max ) ? 12 : 1;
		}

		if ( ! isset( $datetime['day'] ) ) {
			$datetime['day'] = ( $default_to_max ) ? (int) gmdate( 't', mktime( 0, 0, 0, $datetime['month'], 1, $datetime['year'] ) ) : 1;
		}

		if ( ! isset( $datetime['hour'] ) ) {
			$datetime['hour'] = ( $default_to_max ) ? 23 : 0;
		}

		if ( ! isset( $datetime['minute'] ) ) {
			$datetime['minute'] = ( $default_to_max ) ? 59 : 0;
		}

		if ( ! isset( $datetime['second'] ) ) {
			$datetime['second'] = ( $default_to_max ) ? 59 : 0;
		}

		return sprintf( '%04d-%02d-%02d %02d:%02d:%02d', $datetime['year'], $datetime['month'], $datetime['day'], $datetime['hour'], $datetime['minute'], $datetime['second'] );
	}

	/**
	 * Builds a query string for comparing time values (hour, minute, second).
	 *
	 * If just hour, minute, or second is set than a normal comparison will be done.
	 * However if multiple values are passed, a pseudo-decimal time will be created
	 * in order to be able to accurately compare against.
	 *
	 * @since 3.7.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string   $column  The column to query against. Needs to be pre-validated!
	 * @param string   $compare The comparison operator. Needs to be pre-validated!
	 * @param int|null $hour    Optional. An hour value (0-23).
	 * @param int|null $minute  Optional. A minute value (0-59).
	 * @param int|null $second  Optional. A second value (0-59).
	 * @return string|false A query part or false on failure.
	 */
	public function build_time_query( $column, $compare, $hour = null, $minute = null, $second = null ) {
		global $wpdb;

		// Have to have at least one.
		if ( ! isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) {
			return false;
		}

		// Complex combined queries aren't supported for multi-value queries.
		if ( in_array( $compare, array( 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ), true ) ) {
			$return = array();

			$value = $this->build_value( $compare, $hour );
			if ( false !== $value ) {
				$return[] = "HOUR( $column ) $compare $value";
			}

			$value = $this->build_value( $compare, $minute );
			if ( false !== $value ) {
				$return[] = "MINUTE( $column ) $compare $value";
			}

			$value = $this->build_value( $compare, $second );
			if ( false !== $value ) {
				$return[] = "SECOND( $column ) $compare $value";
			}

			return implode( ' AND ', $return );
		}

		// Cases where just one unit is set.
		if ( isset( $hour ) && ! isset( $minute ) && ! isset( $second ) ) {
			$value = $this->build_value( $compare, $hour );
			if ( false !== $value ) {
				return "HOUR( $column ) $compare $value";
			}
		} elseif ( ! isset( $hour ) && isset( $minute ) && ! isset( $second ) ) {
			$value = $this->build_value( $compare, $minute );
			if ( false !== $value ) {
				return "MINUTE( $column ) $compare $value";
			}
		} elseif ( ! isset( $hour ) && ! isset( $minute ) && isset( $second ) ) {
			$value = $this->build_value( $compare, $second );
			if ( false !== $value ) {
				return "SECOND( $column ) $compare $value";
			}
		}

		// Single units were already handled. Since hour & second isn't allowed, minute must to be set.
		if ( ! isset( $minute ) ) {
			return false;
		}

		$format = '';
		$time   = '';

		// Hour.
		if ( null !== $hour ) {
			$format .= '%H.';
			$time   .= sprintf( '%02d', $hour ) . '.';
		} else {
			$format .= '0.';
			$time   .= '0.';
		}

		// Minute.
		$format .= '%i';
		$time   .= sprintf( '%02d', $minute );

		if ( isset( $second ) ) {
			$format .= '%s';
			$time   .= sprintf( '%02d', $second );
		}

		return $wpdb->prepare( "DATE_FORMAT( $column, %s ) $compare %f", $format, $time );
	}

	/**
	 * Sanitizes a 'relation' operator.
	 *
	 * @since 6.0.3
	 *
	 * @param string $relation Raw relation key from the query argument.
	 * @return string Sanitized relation. Either 'AND' or 'OR'.
	 */
	public function sanitize_relation( $relation ) {
		if ( 'OR' === strtoupper( $relation ) ) {
			return 'OR';
		} else {
			return 'AND';
		}
	}
}
                                                                                                                                                                                                             ./wp-includes/class-phpass.php                                                                      0000644 0001750 0001750 00000015163 15152744174 017055  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Portable PHP password hashing framework.
 * @package phpass
 * @since 2.5.0
 * @version 0.5 / WordPress
 * @link https://www.openwall.com/phpass/
 */

#
# Portable PHP password hashing framework.
#
# Version 0.5.4 / WordPress.
#
# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
# the public domain.  Revised in subsequent years, still public domain.
#
# There's absolutely no warranty.
#
# The homepage URL for this framework is:
#
#	http://www.openwall.com/phpass/
#
# Please be sure to update the Version line if you edit this file in any way.
# It is suggested that you leave the main version number intact, but indicate
# your project name (after the slash) and add your own revision information.
#
# Please do not change the "private" password hashing method implemented in
# here, thereby making your hashes incompatible.  However, if you must, please
# change the hash type identifier (the "$P$") to something different.
#
# Obviously, since this code is in the public domain, the above are not
# requirements (there can be none), but merely suggestions.
#

/**
 * Portable PHP password hashing framework.
 *
 * @package phpass
 * @version 0.5 / WordPress
 * @link https://www.openwall.com/phpass/
 * @since 2.5.0
 */
class PasswordHash {
	var $itoa64;
	var $iteration_count_log2;
	var $portable_hashes;
	var $random_state;

	function __construct($iteration_count_log2, $portable_hashes)
	{
		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

		if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) {
			$iteration_count_log2 = 8;
		}
		$this->iteration_count_log2 = $iteration_count_log2;

		$this->portable_hashes = $portable_hashes;

		$this->random_state = microtime();
		if (function_exists('getmypid')) {
			$this->random_state .= getmypid();
		}
	}

	function PasswordHash($iteration_count_log2, $portable_hashes)
	{
		self::__construct($iteration_count_log2, $portable_hashes);
	}

	function get_random_bytes($count)
	{
		$output = '';
		if (@is_readable('/dev/urandom') &&
		    ($fh = @fopen('/dev/urandom', 'rb'))) {
			$output = fread($fh, $count);
			fclose($fh);
		}

		if (strlen($output) < $count) {
			$output = '';
			for ($i = 0; $i < $count; $i += 16) {
				$this->random_state =
				    md5(microtime() . $this->random_state);
				$output .= md5($this->random_state, TRUE);
			}
			$output = substr($output, 0, $count);
		}

		return $output;
	}

	function encode64($input, $count)
	{
		$output = '';
		$i = 0;
		do {
			$value = ord($input[$i++]);
			$output .= $this->itoa64[$value & 0x3f];
			if ($i < $count) {
				$value |= ord($input[$i]) << 8;
			}
			$output .= $this->itoa64[($value >> 6) & 0x3f];
			if ($i++ >= $count) {
				break;
			}
			if ($i < $count) {
				$value |= ord($input[$i]) << 16;
			}
			$output .= $this->itoa64[($value >> 12) & 0x3f];
			if ($i++ >= $count) {
				break;
			}
			$output .= $this->itoa64[($value >> 18) & 0x3f];
		} while ($i < $count);

		return $output;
	}

	function gensalt_private($input)
	{
		$output = '$P$';
		$output .= $this->itoa64[min($this->iteration_count_log2 + 5,
		    30)];
		$output .= $this->encode64($input, 6);

		return $output;
	}

	function crypt_private($password, $setting)
	{
		$output = '*0';
		if (substr($setting, 0, 2) === $output) {
			$output = '*1';
		}

		$id = substr($setting, 0, 3);
		# We use "$P$", phpBB3 uses "$H$" for the same thing
		if ($id !== '$P$' && $id !== '$H$') {
			return $output;
		}

		$count_log2 = strpos($this->itoa64, $setting[3]);
		if ($count_log2 < 7 || $count_log2 > 30) {
			return $output;
		}

		$count = 1 << $count_log2;

		$salt = substr($setting, 4, 8);
		if (strlen($salt) !== 8) {
			return $output;
		}

		# We were kind of forced to use MD5 here since it's the only
		# cryptographic primitive that was available in all versions
		# of PHP in use.  To implement our own low-level crypto in PHP
		# would have resulted in much worse performance and
		# consequently in lower iteration counts and hashes that are
		# quicker to crack (by non-PHP code).
		$hash = md5($salt . $password, TRUE);
		do {
			$hash = md5($hash . $password, TRUE);
		} while (--$count);

		$output = substr($setting, 0, 12);
		$output .= $this->encode64($hash, 16);

		return $output;
	}

	function gensalt_blowfish($input)
	{
		# This one needs to use a different order of characters and a
		# different encoding scheme from the one in encode64() above.
		# We care because the last character in our encoded string will
		# only represent 2 bits.  While two known implementations of
		# bcrypt will happily accept and correct a salt string which
		# has the 4 unused bits set to non-zero, we do not want to take
		# chances and we also do not want to waste an additional byte
		# of entropy.
		$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

		$output = '$2a$';
		$output .= chr((int)(ord('0') + $this->iteration_count_log2 / 10));
		$output .= chr(ord('0') + $this->iteration_count_log2 % 10);
		$output .= '$';

		$i = 0;
		do {
			$c1 = ord($input[$i++]);
			$output .= $itoa64[$c1 >> 2];
			$c1 = ($c1 & 0x03) << 4;
			if ($i >= 16) {
				$output .= $itoa64[$c1];
				break;
			}

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 4;
			$output .= $itoa64[$c1];
			$c1 = ($c2 & 0x0f) << 2;

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 6;
			$output .= $itoa64[$c1];
			$output .= $itoa64[$c2 & 0x3f];
		} while (1);

		return $output;
	}

	function HashPassword($password)
	{
		if ( strlen( $password ) > 4096 ) {
			return '*';
		}

		$random = '';

		if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) {
			$random = $this->get_random_bytes(16);
			$hash =
			    crypt($password, $this->gensalt_blowfish($random));
			if (strlen($hash) === 60) {
				return $hash;
			}
		}

		if (strlen($random) < 6) {
			$random = $this->get_random_bytes(6);
		}
		$hash =
		    $this->crypt_private($password,
		    $this->gensalt_private($random));
		if (strlen($hash) === 34) {
			return $hash;
		}

		# Returning '*' on error is safe here, but would _not_ be safe
		# in a crypt(3)-like function used _both_ for generating new
		# hashes and for validating passwords against existing hashes.
		return '*';
	}

	function CheckPassword($password, $stored_hash)
	{
		if ( strlen( $password ) > 4096 ) {
			return false;
		}

		$hash = $this->crypt_private($password, $stored_hash);
		if ($hash[0] === '*') {
			$hash = crypt($password, $stored_hash);
		}

		# This is not constant-time.  In order to keep the code simple,
		# for timing safety we currently rely on the salts being
		# unpredictable, which they are at least in the non-fallback
		# cases (that is, when we use /dev/urandom and bcrypt).
		return $hash === $stored_hash;
	}
}
                                                                                                                                                                                                                                                                                                                                                                                                             ./wp-includes/class-wp-recovery-mode-email-service.php                                              0000644 0001750 0001750 00000025657 15152744174 023517  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Error Protection API: WP_Recovery_Mode_Email_Link class
 *
 * @package WordPress
 * @since 5.2.0
 */

/**
 * Core class used to send an email with a link to begin Recovery Mode.
 *
 * @since 5.2.0
 */
#[AllowDynamicProperties]
final class WP_Recovery_Mode_Email_Service {

	const RATE_LIMIT_OPTION = 'recovery_mode_email_last_sent';

	/**
	 * Service to generate recovery mode URLs.
	 *
	 * @since 5.2.0
	 * @var WP_Recovery_Mode_Link_Service
	 */
	private $link_service;

	/**
	 * WP_Recovery_Mode_Email_Service constructor.
	 *
	 * @since 5.2.0
	 *
	 * @param WP_Recovery_Mode_Link_Service $link_service
	 */
	public function __construct( WP_Recovery_Mode_Link_Service $link_service ) {
		$this->link_service = $link_service;
	}

	/**
	 * Sends the recovery mode email if the rate limit has not been sent.
	 *
	 * @since 5.2.0
	 *
	 * @param int   $rate_limit Number of seconds before another email can be sent.
	 * @param array $error      Error details from `error_get_last()`.
	 * @param array $extension {
	 *     The extension that caused the error.
	 *
	 *     @type string $slug The extension slug. The plugin or theme's directory.
	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
	 * }
	 * @return true|WP_Error True if email sent, WP_Error otherwise.
	 */
	public function maybe_send_recovery_mode_email( $rate_limit, $error, $extension ) {

		$last_sent = get_option( self::RATE_LIMIT_OPTION );

		if ( ! $last_sent || time() > $last_sent + $rate_limit ) {
			if ( ! update_option( self::RATE_LIMIT_OPTION, time() ) ) {
				return new WP_Error( 'storage_error', __( 'Could not update the email last sent time.' ) );
			}

			$sent = $this->send_recovery_mode_email( $rate_limit, $error, $extension );

			if ( $sent ) {
				return true;
			}

			return new WP_Error(
				'email_failed',
				sprintf(
					/* translators: %s: mail() */
					__( 'The email could not be sent. Possible reason: your host may have disabled the %s function.' ),
					'mail()'
				)
			);
		}

		$err_message = sprintf(
			/* translators: 1: Last sent as a human time diff, 2: Wait time as a human time diff. */
			__( 'A recovery link was already sent %1$s ago. Please wait another %2$s before requesting a new email.' ),
			human_time_diff( $last_sent ),
			human_time_diff( $last_sent + $rate_limit )
		);

		return new WP_Error( 'email_sent_already', $err_message );
	}

	/**
	 * Clears the rate limit, allowing a new recovery mode email to be sent immediately.
	 *
	 * @since 5.2.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function clear_rate_limit() {
		return delete_option( self::RATE_LIMIT_OPTION );
	}

	/**
	 * Sends the Recovery Mode email to the site admin email address.
	 *
	 * @since 5.2.0
	 *
	 * @param int   $rate_limit Number of seconds before another email can be sent.
	 * @param array $error      Error details from `error_get_last()`.
	 * @param array $extension {
	 *     The extension that caused the error.
	 *
	 *     @type string $slug The extension slug. The directory of the plugin or theme.
	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
	 * }
	 * @return bool Whether the email was sent successfully.
	 */
	private function send_recovery_mode_email( $rate_limit, $error, $extension ) {

		$url      = $this->link_service->generate_url();
		$blogname = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );

		$switched_locale = switch_to_locale( get_locale() );

		if ( $extension ) {
			$cause   = $this->get_cause( $extension );
			$details = wp_strip_all_tags( wp_get_extension_error_description( $error ) );

			if ( $details ) {
				$header  = __( 'Error Details' );
				$details = "\n\n" . $header . "\n" . str_pad( '', strlen( $header ), '=' ) . "\n" . $details;
			}
		} else {
			$cause   = '';
			$details = '';
		}

		/**
		 * Filters the support message sent with the the fatal error protection email.
		 *
		 * @since 5.2.0
		 *
		 * @param string $message The Message to include in the email.
		 */
		$support = apply_filters( 'recovery_email_support_info', __( 'Please contact your host for assistance with investigating this issue further.' ) );

		/**
		 * Filters the debug information included in the fatal error protection email.
		 *
		 * @since 5.3.0
		 *
		 * @param array $message An associative array of debug information.
		 */
		$debug = apply_filters( 'recovery_email_debug_info', $this->get_debug( $extension ) );

		/* translators: Do not translate LINK, EXPIRES, CAUSE, DETAILS, SITEURL, PAGEURL, SUPPORT. DEBUG: those are placeholders. */
		$message = __(
			'Howdy!

WordPress has a built-in feature that detects when a plugin or theme causes a fatal error on your site, and notifies you with this automated email.
###CAUSE###
First, visit your website (###SITEURL###) and check for any visible issues. Next, visit the page where the error was caught (###PAGEURL###) and check for any visible issues.

###SUPPORT###

If your site appears broken and you can\'t access your dashboard normally, WordPress now has a special "recovery mode". This lets you safely login to your dashboard and investigate further.

###LINK###

To keep your site safe, this link will expire in ###EXPIRES###. Don\'t worry about that, though: a new link will be emailed to you if the error occurs again after it expires.

When seeking help with this issue, you may be asked for some of the following information:
###DEBUG###

###DETAILS###'
		);
		$message = str_replace(
			array(
				'###LINK###',
				'###EXPIRES###',
				'###CAUSE###',
				'###DETAILS###',
				'###SITEURL###',
				'###PAGEURL###',
				'###SUPPORT###',
				'###DEBUG###',
			),
			array(
				$url,
				human_time_diff( time() + $rate_limit ),
				$cause ? "\n{$cause}\n" : "\n",
				$details,
				home_url( '/' ),
				home_url( $_SERVER['REQUEST_URI'] ),
				$support,
				implode( "\r\n", $debug ),
			),
			$message
		);

		$email = array(
			'to'          => $this->get_recovery_mode_email_address(),
			/* translators: %s: Site title. */
			'subject'     => __( '[%s] Your Site is Experiencing a Technical Issue' ),
			'message'     => $message,
			'headers'     => '',
			'attachments' => '',
		);

		/**
		 * Filters the contents of the Recovery Mode email.
		 *
		 * @since 5.2.0
		 * @since 5.6.0 The `$email` argument includes the `attachments` key.
		 *
		 * @param array  $email {
		 *     Used to build a call to wp_mail().
		 *
		 *     @type string|array $to          Array or comma-separated list of email addresses to send message.
		 *     @type string       $subject     Email subject
		 *     @type string       $message     Message contents
		 *     @type string|array $headers     Optional. Additional headers.
		 *     @type string|array $attachments Optional. Files to attach.
		 * }
		 * @param string $url   URL to enter recovery mode.
		 */
		$email = apply_filters( 'recovery_mode_email', $email, $url );

		$sent = wp_mail(
			$email['to'],
			wp_specialchars_decode( sprintf( $email['subject'], $blogname ) ),
			$email['message'],
			$email['headers'],
			$email['attachments']
		);

		if ( $switched_locale ) {
			restore_previous_locale();
		}

		return $sent;
	}

	/**
	 * Gets the email address to send the recovery mode link to.
	 *
	 * @since 5.2.0
	 *
	 * @return string Email address to send recovery mode link to.
	 */
	private function get_recovery_mode_email_address() {
		if ( defined( 'RECOVERY_MODE_EMAIL' ) && is_email( RECOVERY_MODE_EMAIL ) ) {
			return RECOVERY_MODE_EMAIL;
		}

		return get_option( 'admin_email' );
	}

	/**
	 * Gets the description indicating the possible cause for the error.
	 *
	 * @since 5.2.0
	 *
	 * @param array $extension {
	 *     The extension that caused the error.
	 *
	 *     @type string $slug The extension slug. The directory of the plugin or theme.
	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
	 * }
	 * @return string Message about which extension caused the error.
	 */
	private function get_cause( $extension ) {

		if ( 'plugin' === $extension['type'] ) {
			$plugin = $this->get_plugin( $extension );

			if ( false === $plugin ) {
				$name = $extension['slug'];
			} else {
				$name = $plugin['Name'];
			}

			/* translators: %s: Plugin name. */
			$cause = sprintf( __( 'In this case, WordPress caught an error with one of your plugins, %s.' ), $name );
		} else {
			$theme = wp_get_theme( $extension['slug'] );
			$name  = $theme->exists() ? $theme->display( 'Name' ) : $extension['slug'];

			/* translators: %s: Theme name. */
			$cause = sprintf( __( 'In this case, WordPress caught an error with your theme, %s.' ), $name );
		}

		return $cause;
	}

	/**
	 * Return the details for a single plugin based on the extension data from an error.
	 *
	 * @since 5.3.0
	 *
	 * @param array $extension {
	 *     The extension that caused the error.
	 *
	 *     @type string $slug The extension slug. The directory of the plugin or theme.
	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
	 * }
	 * @return array|false A plugin array {@see get_plugins()} or `false` if no plugin was found.
	 */
	private function get_plugin( $extension ) {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		$plugins = get_plugins();

		// Assume plugin main file name first since it is a common convention.
		if ( isset( $plugins[ "{$extension['slug']}/{$extension['slug']}.php" ] ) ) {
			return $plugins[ "{$extension['slug']}/{$extension['slug']}.php" ];
		} else {
			foreach ( $plugins as $file => $plugin_data ) {
				if ( str_starts_with( $file, "{$extension['slug']}/" ) || $file === $extension['slug'] ) {
					return $plugin_data;
				}
			}
		}

		return false;
	}

	/**
	 * Return debug information in an easy to manipulate format.
	 *
	 * @since 5.3.0
	 *
	 * @param array $extension {
	 *     The extension that caused the error.
	 *
	 *     @type string $slug The extension slug. The directory of the plugin or theme.
	 *     @type string $type The extension type. Either 'plugin' or 'theme'.
	 * }
	 * @return array An associative array of debug information.
	 */
	private function get_debug( $extension ) {
		$theme      = wp_get_theme();
		$wp_version = get_bloginfo( 'version' );

		if ( $extension ) {
			$plugin = $this->get_plugin( $extension );
		} else {
			$plugin = null;
		}

		$debug = array(
			'wp'    => sprintf(
				/* translators: %s: Current WordPress version number. */
				__( 'WordPress version %s' ),
				$wp_version
			),
			'theme' => sprintf(
				/* translators: 1: Current active theme name. 2: Current active theme version. */
				__( 'Active theme: %1$s (version %2$s)' ),
				$theme->get( 'Name' ),
				$theme->get( 'Version' )
			),
		);

		if ( null !== $plugin ) {
			$debug['plugin'] = sprintf(
				/* translators: 1: The failing plugins name. 2: The failing plugins version. */
				__( 'Current plugin: %1$s (version %2$s)' ),
				$plugin['Name'],
				$plugin['Version']
			);
		}

		$debug['php'] = sprintf(
			/* translators: %s: The currently used PHP version. */
			__( 'PHP version %s' ),
			PHP_VERSION
		);

		return $debug;
	}
}
                                                                                 ./wp-includes/compat-utf8.php                                                                       0000644 0001750 0001750 00000046142 15152744165 016624  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php

/**
 * Finds spans of valid and invalid UTF-8 bytes in a given string.
 *
 * This is a low-level tool to power various UTF-8 functionality.
 * It scans through a string until it finds invalid byte spans.
 * When it does this, it does three things:
 *
 *  - Assigns `$at` to the position after the last successful code point.
 *  - Assigns `$invalid_length` to the length of the maximal subpart of
 *    the invalid bytes starting at `$at`.
 *  - Returns how many code points were successfully scanned.
 *
 * This information is enough to build a number of useful UTF-8 functions.
 *
 * Example:
 *
 *     // ñ is U+F1, which in `ISO-8859-1`/`latin1`/`Windows-1252`/`cp1252` is 0xF1.
 *     "Pi\xF1a" === $pineapple = mb_convert_encoding( "Piña", 'Windows-1252', 'UTF-8' );
 *     $at = $invalid_length = 0;
 *
 *     // The first step finds the invalid 0xF1 byte.
 *     2 === _wp_scan_utf8( $pineapple, $at, $invalid_length );
 *     $at === 2; $invalid_length === 1;
 *
 *     // The second step continues to the end of the string.
 *     1 === _wp_scan_utf8( $pineapple, $at, $invalid_length );
 *     $at === 4; $invalid_length === 0;
 *
 * Note! While passing an options array here might be convenient from a calling-code standpoint,
 *       this function is intended to serve as a very low-level foundation upon which to build
 *       higher level functionality. For the sake of keeping costs explicit all arguments are
 *       passed directly.
 *
 * @since 6.9.0
 * @access private
 *
 * @param string    $bytes             UTF-8 encoded string which might include invalid spans of bytes.
 * @param int       $at                Where to start scanning.
 * @param int       $invalid_length    Will be set to how many bytes are to be ignored after `$at`.
 * @param int|null  $max_bytes         Stop scanning after this many bytes have been seen.
 * @param int|null  $max_code_points   Stop scanning after this many code points have been seen.
 * @param bool|null $has_noncharacters Set to indicate if scanned string contained noncharacters.
 * @return int How many code points were successfully scanned.
 */
function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max_bytes = null, ?int $max_code_points = null, ?bool &$has_noncharacters = null ): int {
	$byte_length       = strlen( $bytes );
	$end               = min( $byte_length, $at + ( $max_bytes ?? PHP_INT_MAX ) );
	$invalid_length    = 0;
	$count             = 0;
	$max_count         = $max_code_points ?? PHP_INT_MAX;
	$has_noncharacters = false;

	for ( $i = $at; $i < $end && $count <= $max_count; $i++ ) {
		/*
		 * Quickly skip past US-ASCII bytes, all of which are valid UTF-8.
		 *
		 * This optimization step improves the speed from 10x to 100x
		 * depending on whether the JIT has optimized the function.
		 */
		$ascii_byte_count = strspn(
			$bytes,
			"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
			"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
			" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
			$i,
			$end - $i
		);

		if ( $count + $ascii_byte_count >= $max_count ) {
			$at    = $i + ( $max_count - $count );
			$count = $max_count;
			return $count;
		}

		$count += $ascii_byte_count;
		$i     += $ascii_byte_count;

		if ( $i >= $end ) {
			$at = $end;
			return $count;
		}

		/**
		 * The above fast-track handled all single-byte UTF-8 characters. What
		 * follows MUST be a multibyte sequence otherwise there’s invalid UTF-8.
		 *
		 * Therefore everything past here is checking those multibyte sequences.
		 *
		 * It may look like there’s a need to check against the max bytes here,
		 * but since each match of a single character returns, this functions will
		 * bail already if crossing the max-bytes threshold. This function SHALL
		 * NOT return in the middle of a multi-byte character, so if a character
		 * falls on each side of the max bytes, the entire character will be scanned.
		 *
		 * Because it’s possible that there are truncated characters, the use of
		 * the null-coalescing operator with "\xC0" is a convenience for skipping
		 * length checks on every continuation bytes. This works because 0xC0 is
		 * always invalid in a UTF-8 string, meaning that if the string has been
		 * truncated, it will find 0xC0 and reject as invalid UTF-8.
		 *
		 * > [The following table] lists all of the byte sequences that are well-formed
		 * > in UTF-8. A range of byte values such as A0..BF indicates that any byte
		 * > from A0 to BF (inclusive) is well-formed in that position. Any byte value
		 * > outside of the ranges listed is ill-formed.
		 *
		 * > Table 3-7. Well-Formed UTF-8 Byte Sequences
		 *  ╭─────────────────────┬────────────┬──────────────┬─────────────┬──────────────╮
		 *  │ Code Points         │ First Byte │ Second Byte  │ Third Byte  │ Fourth Byte  │
		 *  ├─────────────────────┼────────────┼──────────────┼─────────────┼──────────────┤
		 *  │ U+0000..U+007F      │ 00..7F     │              │             │              │
		 *  │ U+0080..U+07FF      │ C2..DF     │ 80..BF       │             │              │
		 *  │ U+0800..U+0FFF      │ E0         │ A0..BF       │ 80..BF      │              │
		 *  │ U+1000..U+CFFF      │ E1..EC     │ 80..BF       │ 80..BF      │              │
		 *  │ U+D000..U+D7FF      │ ED         │ 80..9F       │ 80..BF      │              │
		 *  │ U+E000..U+FFFF      │ EE..EF     │ 80..BF       │ 80..BF      │              │
		 *  │ U+10000..U+3FFFF    │ F0         │ 90..BF       │ 80..BF      │ 80..BF       │
		 *  │ U+40000..U+FFFFF    │ F1..F3     │ 80..BF       │ 80..BF      │ 80..BF       │
		 *  │ U+100000..U+10FFFF  │ F4         │ 80..8F       │ 80..BF      │ 80..BF       │
		 *  ╰─────────────────────┴────────────┴──────────────┴─────────────┴──────────────╯
		 *
		 * @see https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G27506
		 */

		// Valid two-byte code points.
		$b1 = ord( $bytes[ $i ] );
		$b2 = ord( $bytes[ $i + 1 ] ?? "\xC0" );

		if ( $b1 >= 0xC2 && $b1 <= 0xDF && $b2 >= 0x80 && $b2 <= 0xBF ) {
			++$count;
			++$i;
			continue;
		}

		// Valid three-byte code points.
		$b3 = ord( $bytes[ $i + 2 ] ?? "\xC0" );

		if ( $b3 < 0x80 || $b3 > 0xBF ) {
			goto invalid_utf8;
		}

		if (
			( 0xE0 === $b1 && $b2 >= 0xA0 && $b2 <= 0xBF ) ||
			( $b1 >= 0xE1 && $b1 <= 0xEC && $b2 >= 0x80 && $b2 <= 0xBF ) ||
			( 0xED === $b1 && $b2 >= 0x80 && $b2 <= 0x9F ) ||
			( $b1 >= 0xEE && $b1 <= 0xEF && $b2 >= 0x80 && $b2 <= 0xBF )
		) {
			++$count;
			$i += 2;

			// Covers the range U+FDD0–U+FDEF, U+FFFE, U+FFFF.
			if ( 0xEF === $b1 ) {
				$has_noncharacters |= (
					( 0xB7 === $b2 && $b3 >= 0x90 && $b3 <= 0xAF ) ||
					( 0xBF === $b2 && ( 0xBE === $b3 || 0xBF === $b3 ) )
				);
			}

			continue;
		}

		// Valid four-byte code points.
		$b4 = ord( $bytes[ $i + 3 ] ?? "\xC0" );

		if ( $b4 < 0x80 || $b4 > 0xBF ) {
			goto invalid_utf8;
		}

		if (
			( 0xF0 === $b1 && $b2 >= 0x90 && $b2 <= 0xBF ) ||
			( $b1 >= 0xF1 && $b1 <= 0xF3 && $b2 >= 0x80 && $b2 <= 0xBF ) ||
			( 0xF4 === $b1 && $b2 >= 0x80 && $b2 <= 0x8F )
		) {
			++$count;
			$i += 3;

			// Covers U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF, …, U+10FFFE, U+10FFFF.
			$has_noncharacters |= (
				( 0x0F === ( $b2 & 0x0F ) ) &&
				0xBF === $b3 &&
				( 0xBE === $b4 || 0xBF === $b4 )
			);

			continue;
		}

		/**
		 * When encountering invalid byte sequences, Unicode suggests finding the
		 * maximal subpart of a text and replacing that subpart with a single
		 * replacement character.
		 *
		 * > This practice is more secure because it does not result in the
		 * > conversion consuming parts of valid sequences as though they were
		 * > invalid. It also guarantees at least one replacement character will
		 * > occur for each instance of an invalid sequence in the original text.
		 * > Furthermore, this practice can be defined consistently for better
		 * > interoperability between different implementations of conversion.
		 *
		 * @see https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G40630
		 */
		invalid_utf8:
		$at             = $i;
		$invalid_length = 1;

		// Single-byte and two-byte characters.
		if ( ( 0x00 === ( $b1 & 0x80 ) ) || ( 0xC0 === ( $b1 & 0xE0 ) ) ) {
			return $count;
		}

		$b2 = ord( $bytes[ $i + 1 ] ?? "\xC0" );
		$b3 = ord( $bytes[ $i + 2 ] ?? "\xC0" );

		// Find the maximal subpart and skip past it.
		if ( 0xE0 === ( $b1 & 0xF0 ) ) {
			// Three-byte characters.
			$b2_valid = (
				( 0xE0 === $b1 && $b2 >= 0xA0 && $b2 <= 0xBF ) ||
				( $b1 >= 0xE1 && $b1 <= 0xEC && $b2 >= 0x80 && $b2 <= 0xBF ) ||
				( 0xED === $b1 && $b2 >= 0x80 && $b2 <= 0x9F ) ||
				( $b1 >= 0xEE && $b1 <= 0xEF && $b2 >= 0x80 && $b2 <= 0xBF )
			);

			$invalid_length = min( $end - $i, $b2_valid ? 2 : 1 );
			return $count;
		} elseif ( 0xF0 === ( $b1 & 0xF8 ) ) {
			// Four-byte characters.
			$b2_valid = (
				( 0xF0 === $b1 && $b2 >= 0x90 && $b2 <= 0xBF ) ||
				( $b1 >= 0xF1 && $b1 <= 0xF3 && $b2 >= 0x80 && $b2 <= 0xBF ) ||
				( 0xF4 === $b1 && $b2 >= 0x80 && $b2 <= 0x8F )
			);

			$b3_valid = $b3 >= 0x80 && $b3 <= 0xBF;

			$invalid_length = min( $end - $i, $b2_valid ? ( $b3_valid ? 3 : 2 ) : 1 );
			return $count;
		}

		return $count;
	}

	$at = $i;
	return $count;
}

/**
 * Fallback mechanism for safely validating UTF-8 bytes.
 *
 * @since 6.9.0
 * @access private
 *
 * @see wp_is_valid_utf8()
 *
 * @param string $bytes String which might contain text encoded as UTF-8.
 * @return bool Whether the provided bytes can decode as valid UTF-8.
 */
function _wp_is_valid_utf8_fallback( string $bytes ): bool {
	$bytes_length = strlen( $bytes );
	if ( 0 === $bytes_length ) {
		return true;
	}

	$next_byte_at   = 0;
	$invalid_length = 0;

	_wp_scan_utf8( $bytes, $next_byte_at, $invalid_length );

	return $bytes_length === $next_byte_at && 0 === $invalid_length;
}

/**
 * Fallback mechanism for replacing invalid spans of UTF-8 bytes.
 *
 * Example:
 *
 *     'Pi�a' === _wp_scrub_utf8_fallback( "Pi\xF1a" ); // “ñ” is 0xF1 in Windows-1252.
 *
 * @since 6.9.0
 * @access private
 *
 * @see wp_scrub_utf8()
 *
 * @param string $bytes UTF-8 encoded string which might contain spans of invalid bytes.
 * @return string Input string with spans of invalid bytes swapped with the replacement character.
 */
function _wp_scrub_utf8_fallback( string $bytes ): string {
	$bytes_length   = strlen( $bytes );
	$next_byte_at   = 0;
	$was_at         = 0;
	$invalid_length = 0;
	$scrubbed       = '';

	while ( $next_byte_at <= $bytes_length ) {
		_wp_scan_utf8( $bytes, $next_byte_at, $invalid_length );

		if ( $next_byte_at >= $bytes_length ) {
			if ( 0 === $was_at ) {
				return $bytes;
			}

			return $scrubbed . substr( $bytes, $was_at, $next_byte_at - $was_at - $invalid_length );
		}

		$scrubbed .= substr( $bytes, $was_at, $next_byte_at - $was_at );
		$scrubbed .= "\u{FFFD}";

		$next_byte_at += $invalid_length;
		$was_at        = $next_byte_at;
	}

	return $scrubbed;
}

/**
 * Returns how many code points are found in the given UTF-8 string.
 *
 * Invalid spans of bytes count as a single code point according
 * to the maximal subpart rule. This function is a fallback method
 * for calling `mb_strlen( $text, 'UTF-8' )`.
 *
 * When negative values are provided for the byte offsets or length,
 * this will always report zero code points.
 *
 * Example:
 *
 *     4  === _wp_utf8_codepoint_count( 'text' );
 *
 *     // Groups are 'test', "\x90" as '�', 'wp', "\xE2\x80" as '�', "\xC0" as '�', and 'test'.
 *     13 === _wp_utf8_codepoint_count( "test\x90wp\xE2\x80\xC0test" );
 *
 * @since 6.9.0
 * @access private
 *
 * @param string $text            Count code points in this string.
 * @param ?int   $byte_offset     Start counting after this many bytes in `$text`. Must be positive.
 * @param ?int   $max_byte_length Optional. Stop counting after having scanned past this many bytes.
 *                                Default is to scan until the end of the string. Must be positive.
 * @return int How many code points were found.
 */
function _wp_utf8_codepoint_count( string $text, ?int $byte_offset = 0, ?int $max_byte_length = PHP_INT_MAX ): int {
	if ( $byte_offset < 0 ) {
		return 0;
	}

	$count           = 0;
	$at              = $byte_offset;
	$end             = strlen( $text );
	$invalid_length  = 0;
	$max_byte_length = min( $end - $at, $max_byte_length );

	while ( $at < $end && ( $at - $byte_offset ) < $max_byte_length ) {
		$count += _wp_scan_utf8( $text, $at, $invalid_length, $max_byte_length - ( $at - $byte_offset ) );
		$count += $invalid_length > 0 ? 1 : 0;
		$at    += $invalid_length;
	}

	return $count;
}

/**
 * Given a starting offset within a string and a maximum number of code points,
 * return how many bytes are occupied by the span of characters.
 *
 * Invalid spans of bytes count as a single code point according to the maximal
 * subpart rule. This function is a fallback method for calling
 * `strlen( mb_substr( substr( $text, $at ), 0, $max_code_points ) )`.
 *
 * @since 6.9.0
 * @access private
 *
 * @param string $text              Count bytes of span in this text.
 * @param int    $byte_offset       Start counting at this byte offset.
 * @param int    $max_code_points   Stop counting after this many code points have been seen,
 *                                  or at the end of the string.
 * @param ?int   $found_code_points Optional. Will be set to number of found code points in
 *                                  span, as this might be smaller than the maximum count if
 *                                  the string is not long enough.
 * @return int Number of bytes spanned by the code points.
 */
function _wp_utf8_codepoint_span( string $text, int $byte_offset, int $max_code_points, ?int &$found_code_points = 0 ): int {
	$was_at            = $byte_offset;
	$invalid_length    = 0;
	$end               = strlen( $text );
	$found_code_points = 0;

	while ( $byte_offset < $end && $found_code_points < $max_code_points ) {
		$needed      = $max_code_points - $found_code_points;
		$chunk_count = _wp_scan_utf8( $text, $byte_offset, $invalid_length, null, $needed );

		$found_code_points += $chunk_count;

		// Invalid spans only convey one code point count regardless of how long they are.
		if ( 0 !== $invalid_length && $found_code_points < $max_code_points ) {
			++$found_code_points;
			$byte_offset += $invalid_length;
		}
	}

	return $byte_offset - $was_at;
}

/**
 * Fallback support for determining if a string contains Unicode noncharacters.
 *
 * @since 6.9.0
 * @access private
 *
 * @see \wp_has_noncharacters()
 *
 * @param string $text Are there noncharacters in this string?
 * @return bool Whether noncharacters were found in the string.
 */
function _wp_has_noncharacters_fallback( string $text ): bool {
	$at                = 0;
	$invalid_length    = 0;
	$has_noncharacters = false;
	$end               = strlen( $text );

	while ( $at < $end && ! $has_noncharacters ) {
		_wp_scan_utf8( $text, $at, $invalid_length, null, null, $has_noncharacters );
		$at += $invalid_length;
	}

	return $has_noncharacters;
}

/**
 * Converts a string from ISO-8859-1 to UTF-8, maintaining backwards compatibility
 * with the deprecated function from the PHP standard library.
 *
 * @since 6.9.0
 * @access private
 *
 * @see \utf8_encode()
 *
 * @param string $iso_8859_1_text Text treated as ISO-8859-1 (latin1) bytes.
 * @return string Text converted into UTF-8.
 */
function _wp_utf8_encode_fallback( $iso_8859_1_text ) {
	$iso_8859_1_text = (string) $iso_8859_1_text;
	$at              = 0;
	$was_at          = 0;
	$end             = strlen( $iso_8859_1_text );
	$utf8            = '';

	while ( $at < $end ) {
		// US-ASCII bytes are identical in ISO-8859-1 and UTF-8. These are 0x00–0x7F.
		$ascii_byte_count = strspn(
			$iso_8859_1_text,
			"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
			"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
			" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
			$at
		);

		if ( $ascii_byte_count > 0 ) {
			$at += $ascii_byte_count;
			continue;
		}

		// All other bytes transform into two-byte UTF-8 sequences.
		$code_point = ord( $iso_8859_1_text[ $at ] );
		$byte1      = chr( 0xC0 | ( $code_point >> 6 ) );
		$byte2      = chr( 0x80 | ( $code_point & 0x3F ) );

		$utf8 .= substr( $iso_8859_1_text, $was_at, $at - $was_at );
		$utf8 .= "{$byte1}{$byte2}";

		++$at;
		$was_at = $at;
	}

	if ( 0 === $was_at ) {
		return $iso_8859_1_text;
	}

	$utf8 .= substr( $iso_8859_1_text, $was_at );
	return $utf8;
}

/**
 * Converts a string from UTF-8 to ISO-8859-1, maintaining backwards compatibility
 * with the deprecated function from the PHP standard library.
 *
 * @since 6.9.0
 * @access private
 *
 * @see \utf8_decode()
 *
 * @param string $utf8_text Text treated as UTF-8 bytes.
 * @return string Text converted into ISO-8859-1.
 */
function _wp_utf8_decode_fallback( $utf8_text ) {
	$utf8_text       = (string) $utf8_text;
	$at              = 0;
	$was_at          = 0;
	$end             = strlen( $utf8_text );
	$iso_8859_1_text = '';

	while ( $at < $end ) {
		// US-ASCII bytes are identical in ISO-8859-1 and UTF-8. These are 0x00–0x7F.
		$ascii_byte_count = strspn(
			$utf8_text,
			"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" .
			"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
			" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
			$at
		);

		if ( $ascii_byte_count > 0 ) {
			$at += $ascii_byte_count;
			continue;
		}

		$next_at        = $at;
		$invalid_length = 0;
		$found          = _wp_scan_utf8( $utf8_text, $next_at, $invalid_length, null, 1 );
		$span_length    = $next_at - $at;
		$next_byte      = '?';

		if ( 1 !== $found ) {
			if ( $invalid_length > 0 ) {
				$next_byte = '';
				goto flush_sub_part;
			}

			break;
		}

		// All convertible code points are two-bytes long.
		$byte1 = ord( $utf8_text[ $at ] );
		if ( 0xC0 !== ( $byte1 & 0xE0 ) ) {
			goto flush_sub_part;
		}

		// All convertible code points are not greater than U+FF.
		$byte2      = ord( $utf8_text[ $at + 1 ] );
		$code_point = ( ( $byte1 & 0x1F ) << 6 ) | ( ( $byte2 & 0x3F ) );
		if ( $code_point > 0xFF ) {
			goto flush_sub_part;
		}

		$next_byte = chr( $code_point );

		flush_sub_part:
		$iso_8859_1_text .= substr( $utf8_text, $was_at, $at - $was_at );
		$iso_8859_1_text .= $next_byte;
		$at              += $span_length;
		$was_at           = $at;

		if ( $invalid_length > 0 ) {
			$iso_8859_1_text .= '?';
			$at              += $invalid_length;
			$was_at           = $at;
		}
	}

	if ( 0 === $was_at ) {
		return $utf8_text;
	}

	$iso_8859_1_text .= substr( $utf8_text, $was_at );
	return $iso_8859_1_text;
}
                                                                                                                                                                                                                                                                                                                                                                                                                              ./wp-includes/class-wp-navigation-fallback.php                                                      0000644 0001750 0001750 00000021751 15152744174 022077  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * WP_Navigation_Fallback class
 *
 * Manages fallback behavior for Navigation menus.
 *
 * @package WordPress
 * @subpackage Navigation
 * @since 6.3.0
 */

/**
 * Manages fallback behavior for Navigation menus.
 *
 * @since 6.3.0
 */
class WP_Navigation_Fallback {

	/**
	 * Updates the wp_navigation custom post type schema, in order to expose
	 * additional fields in the embeddable links of WP_REST_Navigation_Fallback_Controller.
	 *
	 * The Navigation Fallback endpoint may embed the full Navigation Menu object
	 * into the response as the `self` link. By default, the Posts Controller
	 * will only expose a limited subset of fields but the editor requires
	 * additional fields to be available in order to utilize the menu.
	 *
	 * Used with the `rest_wp_navigation_item_schema` hook.
	 *
	 * @since 6.4.0
	 *
	 * @param array $schema The schema for the `wp_navigation` post.
	 * @return array The modified schema.
	 */
	public static function update_wp_navigation_post_schema( $schema ) {
		// Expose top level fields.
		$schema['properties']['status']['context']  = array_merge( $schema['properties']['status']['context'], array( 'embed' ) );
		$schema['properties']['content']['context'] = array_merge( $schema['properties']['content']['context'], array( 'embed' ) );

		/*
		 * Exposes sub properties of content field.
		 * These sub properties aren't exposed by the posts controller by default,
		 * for requests where context is `embed`.
		 *
		 * @see WP_REST_Posts_Controller::get_item_schema()
		 */
		$schema['properties']['content']['properties']['raw']['context']           = array_merge( $schema['properties']['content']['properties']['raw']['context'], array( 'embed' ) );
		$schema['properties']['content']['properties']['rendered']['context']      = array_merge( $schema['properties']['content']['properties']['rendered']['context'], array( 'embed' ) );
		$schema['properties']['content']['properties']['block_version']['context'] = array_merge( $schema['properties']['content']['properties']['block_version']['context'], array( 'embed' ) );

		/*
		 * Exposes sub properties of title field.
		 * These sub properties aren't exposed by the posts controller by default,
		 * for requests where context is `embed`.
		 *
		 * @see WP_REST_Posts_Controller::get_item_schema()
		 */
		$schema['properties']['title']['properties']['raw']['context'] = array_merge( $schema['properties']['title']['properties']['raw']['context'], array( 'embed' ) );

		return $schema;
	}

	/**
	 * Gets (and/or creates) an appropriate fallback Navigation Menu.
	 *
	 * @since 6.3.0
	 *
	 * @return WP_Post|null the fallback Navigation Post or null.
	 */
	public static function get_fallback() {
		/**
		 * Filters whether or not a fallback should be created.
		 *
		 * @since 6.3.0
		 *
		 * @param bool $create Whether to create a fallback navigation menu. Default true.
		 */
		$should_create_fallback = apply_filters( 'wp_navigation_should_create_fallback', true );

		$fallback = static::get_most_recently_published_navigation();

		if ( $fallback || ! $should_create_fallback ) {
			return $fallback;
		}

		$fallback = static::create_classic_menu_fallback();

		if ( $fallback && ! is_wp_error( $fallback ) ) {
			// Return the newly created fallback post object which will now be the most recently created navigation menu.
			return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
		}

		$fallback = static::create_default_fallback();

		if ( $fallback && ! is_wp_error( $fallback ) ) {
			// Return the newly created fallback post object which will now be the most recently created navigation menu.
			return $fallback instanceof WP_Post ? $fallback : static::get_most_recently_published_navigation();
		}

		return null;
	}

	/**
	 * Finds the most recently published `wp_navigation` post type.
	 *
	 * @since 6.3.0
	 *
	 * @return WP_Post|null the first non-empty Navigation or null.
	 */
	private static function get_most_recently_published_navigation() {

		$parsed_args = array(
			'post_type'              => 'wp_navigation',
			'no_found_rows'          => true,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
			'order'                  => 'DESC',
			'orderby'                => 'date',
			'post_status'            => 'publish',
			'posts_per_page'         => 1,
		);

		$navigation_post = new WP_Query( $parsed_args );

		if ( count( $navigation_post->posts ) > 0 ) {
			return $navigation_post->posts[0];
		}

		return null;
	}

	/**
	 * Creates a Navigation Menu post from a Classic Menu.
	 *
	 * @since 6.3.0
	 *
	 * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
	 */
	private static function create_classic_menu_fallback() {
		// See if we have a classic menu.
		$classic_nav_menu = static::get_fallback_classic_menu();

		if ( ! $classic_nav_menu ) {
			return new WP_Error( 'no_classic_menus', __( 'No Classic Menus found.' ) );
		}

		// If there is a classic menu then convert it to blocks.
		$classic_nav_menu_blocks = WP_Classic_To_Block_Menu_Converter::convert( $classic_nav_menu );

		if ( is_wp_error( $classic_nav_menu_blocks ) ) {
			return $classic_nav_menu_blocks;
		}

		if ( empty( $classic_nav_menu_blocks ) ) {
			return new WP_Error( 'cannot_convert_classic_menu', __( 'Unable to convert Classic Menu to blocks.' ) );
		}

		// Create a new navigation menu from the classic menu.
		$classic_menu_fallback = wp_insert_post(
			array(
				'post_content' => $classic_nav_menu_blocks,
				'post_title'   => $classic_nav_menu->name,
				'post_name'    => $classic_nav_menu->slug,
				'post_status'  => 'publish',
				'post_type'    => 'wp_navigation',
			),
			true // So that we can check whether the result is an error.
		);

		return $classic_menu_fallback;
	}

	/**
	 * Determines the most appropriate classic navigation menu to use as a fallback.
	 *
	 * @since 6.3.0
	 *
	 * @return WP_Term|null The most appropriate classic navigation menu to use as a fallback.
	 */
	private static function get_fallback_classic_menu() {
		$classic_nav_menus = wp_get_nav_menus();

		if ( ! $classic_nav_menus || is_wp_error( $classic_nav_menus ) ) {
			return null;
		}

		$nav_menu = static::get_nav_menu_at_primary_location();

		if ( $nav_menu ) {
			return $nav_menu;
		}

		$nav_menu = static::get_nav_menu_with_primary_slug( $classic_nav_menus );

		if ( $nav_menu ) {
			return $nav_menu;
		}

		return static::get_most_recently_created_nav_menu( $classic_nav_menus );
	}


	/**
	 * Sorts the classic menus and returns the most recently created one.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
	 * @return WP_Term The most recently created classic nav menu.
	 */
	private static function get_most_recently_created_nav_menu( $classic_nav_menus ) {
		usort(
			$classic_nav_menus,
			static function ( $a, $b ) {
				return $b->term_id - $a->term_id;
			}
		);

		return $classic_nav_menus[0];
	}

	/**
	 * Returns the classic menu with the slug `primary` if it exists.
	 *
	 * @since 6.3.0
	 *
	 * @param WP_Term[] $classic_nav_menus Array of classic nav menu term objects.
	 * @return WP_Term|null The classic nav menu with the slug `primary` or null.
	 */
	private static function get_nav_menu_with_primary_slug( $classic_nav_menus ) {
		foreach ( $classic_nav_menus as $classic_nav_menu ) {
			if ( 'primary' === $classic_nav_menu->slug ) {
				return $classic_nav_menu;
			}
		}

		return null;
	}


	/**
	 * Gets the classic menu assigned to the `primary` navigation menu location
	 * if it exists.
	 *
	 * @since 6.3.0
	 *
	 * @return WP_Term|null The classic nav menu assigned to the `primary` location or null.
	 */
	private static function get_nav_menu_at_primary_location() {
		$locations = get_nav_menu_locations();

		if ( isset( $locations['primary'] ) ) {
			$primary_menu = wp_get_nav_menu_object( $locations['primary'] );

			if ( $primary_menu ) {
				return $primary_menu;
			}
		}

		return null;
	}

	/**
	 * Creates a default Navigation Block Menu fallback.
	 *
	 * @since 6.3.0
	 *
	 * @return int|WP_Error The post ID of the default fallback menu or a WP_Error object.
	 */
	private static function create_default_fallback() {

		$default_blocks = static::get_default_fallback_blocks();

		// Create a new navigation menu from the fallback blocks.
		$default_fallback = wp_insert_post(
			array(
				'post_content' => $default_blocks,
				'post_title'   => _x( 'Navigation', 'Title of a Navigation menu' ),
				'post_name'    => 'navigation',
				'post_status'  => 'publish',
				'post_type'    => 'wp_navigation',
			),
			true // So that we can check whether the result is an error.
		);

		return $default_fallback;
	}

	/**
	 * Gets the rendered markup for the default fallback blocks.
	 *
	 * @since 6.3.0
	 *
	 * @return string default blocks markup to use a the fallback.
	 */
	private static function get_default_fallback_blocks() {
		$registry = WP_Block_Type_Registry::get_instance();

		// If `core/page-list` is not registered then use empty blocks.
		return $registry->is_registered( 'core/page-list' ) ? '<!-- wp:page-list /-->' : '';
	}
}
                       ./wp-includes/ms-default-filters.php                                                                0000644 0001750 0001750 00000014754 15152744173 020167  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Sets up the default filters and actions for Multisite.
 *
 * If you need to remove a default hook, this file will give you the priority
 * for which to use to remove the hook.
 *
 * Not all of the Multisite default hooks are found in ms-default-filters.php
 *
 * @package WordPress
 * @subpackage Multisite
 * @see default-filters.php
 * @since 3.0.0
 */

add_action( 'init', 'ms_subdomain_constants' );

// Functions.
add_action( 'update_option_blog_public', 'update_blog_public', 10, 2 );
add_filter( 'option_users_can_register', 'users_can_register_signup_filter' );
add_filter( 'site_option_welcome_user_email', 'welcome_user_msg_filter' );

// Users.
add_filter( 'wpmu_validate_user_signup', 'signup_nonce_check' );
add_action( 'init', 'maybe_add_existing_user_to_blog' );
add_action( 'wpmu_new_user', 'newuser_notify_siteadmin' );
add_action( 'wpmu_activate_user', 'add_new_user_to_blog', 10, 3 );
add_action( 'wpmu_activate_user', 'wpmu_welcome_user_notification', 10, 3 );
add_action( 'after_signup_user', 'wpmu_signup_user_notification', 10, 4 );
add_action( 'network_site_new_created_user', 'wp_send_new_user_notifications' );
add_action( 'network_site_users_created_user', 'wp_send_new_user_notifications' );
add_action( 'network_user_new_created_user', 'wp_send_new_user_notifications' );
add_filter( 'sanitize_user', 'strtolower' );
add_action( 'deleted_user', 'wp_delete_signup_on_user_delete', 10, 3 );

// Roles.
add_action( 'switch_blog', 'wp_switch_roles_and_user', 1, 2 );

// Blogs.
add_filter( 'wpmu_validate_blog_signup', 'signup_nonce_check' );
add_action( 'wpmu_activate_blog', 'wpmu_welcome_notification', 10, 5 );
add_action( 'after_signup_site', 'wpmu_signup_blog_notification', 10, 7 );
add_filter( 'wp_normalize_site_data', 'wp_normalize_site_data', 10, 1 );
add_action( 'wp_validate_site_data', 'wp_validate_site_data', 10, 3 );
add_action( 'wp_insert_site', 'wp_maybe_update_network_site_counts_on_update', 10, 1 );
add_action( 'wp_update_site', 'wp_maybe_update_network_site_counts_on_update', 10, 2 );
add_action( 'wp_delete_site', 'wp_maybe_update_network_site_counts_on_update', 10, 1 );
add_action( 'wp_insert_site', 'wp_maybe_transition_site_statuses_on_update', 10, 1 );
add_action( 'wp_update_site', 'wp_maybe_transition_site_statuses_on_update', 10, 2 );
add_action( 'wp_update_site', 'wp_maybe_clean_new_site_cache_on_update', 10, 2 );
add_action( 'wp_initialize_site', 'wp_initialize_site', 10, 2 );
add_action( 'wp_initialize_site', 'wpmu_log_new_registrations', 100, 2 );
add_action( 'wp_initialize_site', 'newblog_notify_siteadmin', 100, 1 );
add_action( 'wp_uninitialize_site', 'wp_uninitialize_site', 10, 1 );
add_action( 'update_blog_public', 'wp_update_blog_public_option_on_site_update', 1, 2 );

// Site meta.
add_action( 'added_blog_meta', 'wp_cache_set_sites_last_changed' );
add_action( 'updated_blog_meta', 'wp_cache_set_sites_last_changed' );
add_action( 'deleted_blog_meta', 'wp_cache_set_sites_last_changed' );
add_filter( 'get_blog_metadata', 'wp_check_site_meta_support_prefilter' );
add_filter( 'add_blog_metadata', 'wp_check_site_meta_support_prefilter' );
add_filter( 'update_blog_metadata', 'wp_check_site_meta_support_prefilter' );
add_filter( 'delete_blog_metadata', 'wp_check_site_meta_support_prefilter' );
add_filter( 'get_blog_metadata_by_mid', 'wp_check_site_meta_support_prefilter' );
add_filter( 'update_blog_metadata_by_mid', 'wp_check_site_meta_support_prefilter' );
add_filter( 'delete_blog_metadata_by_mid', 'wp_check_site_meta_support_prefilter' );
add_filter( 'update_blog_metadata_cache', 'wp_check_site_meta_support_prefilter' );

// Register nonce.
add_action( 'signup_hidden_fields', 'signup_nonce_fields' );

// Template.
add_action( 'template_redirect', 'maybe_redirect_404' );
add_filter( 'allowed_redirect_hosts', 'redirect_this_site' );

// Administration.
add_action( 'after_delete_post', '_update_posts_count_on_delete', 10, 2 );
add_action( 'delete_post', '_update_blog_date_on_post_delete' );
add_action( 'transition_post_status', '_update_blog_date_on_post_publish', 10, 3 );
add_action( 'transition_post_status', '_update_posts_count_on_transition_post_status', 10, 3 );

// Counts.
add_action( 'admin_init', 'wp_schedule_update_network_counts' );
add_action( 'update_network_counts', 'wp_update_network_counts', 10, 0 );
foreach ( array( 'wpmu_new_user', 'make_spam_user', 'make_ham_user' ) as $action ) {
	add_action( $action, 'wp_maybe_update_network_user_counts', 10, 0 );
}

// These counts are handled by wp_update_network_counts() on Multisite:
remove_action( 'admin_init', 'wp_schedule_update_user_counts' );
remove_action( 'wp_update_user_counts', 'wp_schedule_update_user_counts' );

foreach ( array( 'make_spam_blog', 'make_ham_blog', 'archive_blog', 'unarchive_blog', 'make_delete_blog', 'make_undelete_blog' ) as $action ) {
	add_action( $action, 'wp_maybe_update_network_site_counts', 10, 0 );
}
unset( $action );

// Files.
add_filter( 'wp_upload_bits', 'upload_is_file_too_big' );
add_filter( 'import_upload_size_limit', 'fix_import_form_size' );
add_filter( 'upload_mimes', 'check_upload_mimes' );
add_filter( 'upload_size_limit', 'upload_size_limit_filter' );
add_action( 'upload_ui_over_quota', 'multisite_over_quota_message' );

// Mail.
add_action( 'phpmailer_init', 'fix_phpmailer_messageid' );

// Disable somethings by default for multisite.
add_filter( 'enable_update_services_configuration', '__return_false' );
if ( ! defined( 'POST_BY_EMAIL' ) || ! POST_BY_EMAIL ) { // Back compat constant.
	add_filter( 'enable_post_by_email_configuration', '__return_false' );
}
if ( ! defined( 'EDIT_ANY_USER' ) || ! EDIT_ANY_USER ) { // Back compat constant.
	add_filter( 'enable_edit_any_user_configuration', '__return_false' );
}
add_filter( 'force_filtered_html_on_import', '__return_true' );

// WP_HOME and WP_SITEURL should not have any effect in MS.
remove_filter( 'option_siteurl', '_config_wp_siteurl' );
remove_filter( 'option_home', '_config_wp_home' );

// Some options changes should trigger site details refresh.
add_action( 'update_option_blogname', 'clean_site_details_cache', 10, 0 );
add_action( 'update_option_siteurl', 'clean_site_details_cache', 10, 0 );
add_action( 'update_option_post_count', 'clean_site_details_cache', 10, 0 );
add_action( 'update_option_home', 'clean_site_details_cache', 10, 0 );

// If the network upgrade hasn't run yet, assume ms-files.php rewriting is used.
add_filter( 'default_site_option_ms_files_rewriting', '__return_true' );

// Allow multisite domains for HTTP requests.
add_filter( 'http_request_host_is_external', 'ms_allowed_http_request_hosts', 20, 2 );
                    ./wp-includes/class-wp-comment-query.php                                                            0000644 0001750 0001750 00000137244 15152744177 021020  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Comment API: WP_Comment_Query class
 *
 * @package WordPress
 * @subpackage Comments
 * @since 4.4.0
 */

/**
 * Core class used for querying comments.
 *
 * @since 3.1.0
 *
 * @see WP_Comment_Query::__construct() for accepted arguments.
 */
#[AllowDynamicProperties]
class WP_Comment_Query {

	/**
	 * SQL for database query.
	 *
	 * @since 4.0.1
	 * @var string
	 */
	public $request;

	/**
	 * Metadata query container
	 *
	 * @since 3.5.0
	 * @var WP_Meta_Query A meta query instance.
	 */
	public $meta_query = false;

	/**
	 * Metadata query clauses.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $meta_query_clauses;

	/**
	 * SQL query clauses.
	 *
	 * @since 4.4.0
	 * @var array
	 */
	protected $sql_clauses = array(
		'select'  => '',
		'from'    => '',
		'where'   => array(),
		'groupby' => '',
		'orderby' => '',
		'limits'  => '',
	);

	/**
	 * SQL WHERE clause.
	 *
	 * Stored after the {@see 'comments_clauses'} filter is run on the compiled WHERE sub-clauses.
	 *
	 * @since 4.4.2
	 * @var string
	 */
	protected $filtered_where_clause;

	/**
	 * Date query container
	 *
	 * @since 3.7.0
	 * @var WP_Date_Query A date query instance.
	 */
	public $date_query = false;

	/**
	 * Query vars set by the user.
	 *
	 * @since 3.1.0
	 * @var array
	 */
	public $query_vars;

	/**
	 * Default values for query vars.
	 *
	 * @since 4.2.0
	 * @var array
	 */
	public $query_var_defaults;

	/**
	 * List of comments located by the query.
	 *
	 * @since 4.0.0
	 * @var int[]|WP_Comment[]
	 */
	public $comments;

	/**
	 * The amount of found comments for the current query.
	 *
	 * @since 4.4.0
	 * @var int
	 */
	public $found_comments = 0;

	/**
	 * The number of pages.
	 *
	 * @since 4.4.0
	 * @var int
	 */
	public $max_num_pages = 0;

	/**
	 * Make private/protected methods readable for backward compatibility.
	 *
	 * @since 4.0.0
	 *
	 * @param string $name      Method to call.
	 * @param array  $arguments Arguments to pass when calling.
	 * @return mixed|false Return value of the callback, false otherwise.
	 */
	public function __call( $name, $arguments ) {
		if ( 'get_search_sql' === $name ) {
			return $this->get_search_sql( ...$arguments );
		}
		return false;
	}

	/**
	 * Constructor.
	 *
	 * Sets up the comment query, based on the query vars passed.
	 *
	 * @since 4.2.0
	 * @since 4.4.0 `$parent__in` and `$parent__not_in` were added.
	 * @since 4.4.0 Order by `comment__in` was added. `$update_comment_meta_cache`, `$no_found_rows`,
	 *              `$hierarchical`, and `$update_comment_post_cache` were added.
	 * @since 4.5.0 Introduced the `$author_url` argument.
	 * @since 4.6.0 Introduced the `$cache_domain` argument.
	 * @since 4.9.0 Introduced the `$paged` argument.
	 * @since 5.1.0 Introduced the `$meta_compare_key` argument.
	 * @since 5.3.0 Introduced the `$meta_type_key` argument.
	 *
	 * @param string|array $query {
	 *     Optional. Array or query string of comment query parameters. Default empty.
	 *
	 *     @type string          $author_email              Comment author email address. Default empty.
	 *     @type string          $author_url                Comment author URL. Default empty.
	 *     @type int[]           $author__in                Array of author IDs to include comments for. Default empty.
	 *     @type int[]           $author__not_in            Array of author IDs to exclude comments for. Default empty.
	 *     @type int[]           $comment__in               Array of comment IDs to include. Default empty.
	 *     @type int[]           $comment__not_in           Array of comment IDs to exclude. Default empty.
	 *     @type bool            $count                     Whether to return a comment count (true) or array of
	 *                                                      comment objects (false). Default false.
	 *     @type array           $date_query                Date query clauses to limit comments by. See WP_Date_Query.
	 *                                                      Default null.
	 *     @type string          $fields                    Comment fields to return. Accepts 'ids' for comment IDs
	 *                                                      only or empty for all fields. Default empty.
	 *     @type array           $include_unapproved        Array of IDs or email addresses of users whose unapproved
	 *                                                      comments will be returned by the query regardless of
	 *                                                      `$status`. Default empty.
	 *     @type int             $karma                     Karma score to retrieve matching comments for.
	 *                                                      Default empty.
	 *     @type string|string[] $meta_key                  Meta key or keys to filter by.
	 *     @type string|string[] $meta_value                Meta value or values to filter by.
	 *     @type string          $meta_compare              MySQL operator used for comparing the meta value.
	 *                                                      See WP_Meta_Query::__construct() for accepted values and default value.
	 *     @type string          $meta_compare_key          MySQL operator used for comparing the meta key.
	 *                                                      See WP_Meta_Query::__construct() for accepted values and default value.
	 *     @type string          $meta_type                 MySQL data type that the meta_value column will be CAST to for comparisons.
	 *                                                      See WP_Meta_Query::__construct() for accepted values and default value.
	 *     @type string          $meta_type_key             MySQL data type that the meta_key column will be CAST to for comparisons.
	 *                                                      See WP_Meta_Query::__construct() for accepted values and default value.
	 *     @type array           $meta_query                An associative array of WP_Meta_Query arguments.
	 *                                                      See WP_Meta_Query::__construct() for accepted values.
	 *     @type int             $number                    Maximum number of comments to retrieve.
	 *                                                      Default empty (no limit).
	 *     @type int             $paged                     When used with `$number`, defines the page of results to return.
	 *                                                      When used with `$offset`, `$offset` takes precedence. Default 1.
	 *     @type int             $offset                    Number of comments to offset the query. Used to build
	 *                                                      LIMIT clause. Default 0.
	 *     @type bool            $no_found_rows             Whether to disable the `SQL_CALC_FOUND_ROWS` query.
	 *                                                      Default: true.
	 *     @type string|array    $orderby                   Comment status or array of statuses. To use 'meta_value'
	 *                                                      or 'meta_value_num', `$meta_key` must also be defined.
	 *                                                      To sort by a specific `$meta_query` clause, use that
	 *                                                      clause's array key. Accepts:
	 *                                                      - 'comment_agent'
	 *                                                      - 'comment_approved'
	 *                                                      - 'comment_author'
	 *                                                      - 'comment_author_email'
	 *                                                      - 'comment_author_IP'
	 *                                                      - 'comment_author_url'
	 *                                                      - 'comment_content'
	 *                                                      - 'comment_date'
	 *                                                      - 'comment_date_gmt'
	 *                                                      - 'comment_ID'
	 *                                                      - 'comment_karma'
	 *                                                      - 'comment_parent'
	 *                                                      - 'comment_post_ID'
	 *                                                      - 'comment_type'
	 *                                                      - 'user_id'
	 *                                                      - 'comment__in'
	 *                                                      - 'meta_value'
	 *                                                      - 'meta_value_num'
	 *                                                      - The value of `$meta_key`
	 *                                                      - The array keys of `$meta_query`
	 *                                                      - false, an empty array, or 'none' to disable `ORDER BY` clause.
	 *                                                      Default: 'comment_date_gmt'.
	 *     @type string          $order                     How to order retrieved comments. Accepts 'ASC', 'DESC'.
	 *                                                      Default: 'DESC'.
	 *     @type int             $parent                    Parent ID of comment to retrieve children of.
	 *                                                      Default empty.
	 *     @type int[]           $parent__in                Array of parent IDs of comments to retrieve children for.
	 *                                                      Default empty.
	 *     @type int[]           $parent__not_in            Array of parent IDs of comments *not* to retrieve
	 *                                                      children for. Default empty.
	 *     @type int[]           $post_author__in           Array of author IDs to retrieve comments for.
	 *                                                      Default empty.
	 *     @type int[]           $post_author__not_in       Array of author IDs *not* to retrieve comments for.
	 *                                                      Default empty.
	 *     @type int             $post_id                   Limit results to those affiliated with a given post ID.
	 *                                                      Default 0.
	 *     @type int[]           $post__in                  Array of post IDs to include affiliated comments for.
	 *                                                      Default empty.
	 *     @type int[]           $post__not_in              Array of post IDs to exclude affiliated comments for.
	 *                                                      Default empty.
	 *     @type int             $post_author               Post author ID to limit results by. Default empty.
	 *     @type string|string[] $post_status               Post status or array of post statuses to retrieve
	 *                                                      affiliated comments for. Pass 'any' to match any value.
	 *                                                      Default empty.
	 *     @type string|string[] $post_type                 Post type or array of post types to retrieve affiliated
	 *                                                      comments for. Pass 'any' to match any value. Default empty.
	 *     @type string          $post_name                 Post name to retrieve affiliated comments for.
	 *                                                      Default empty.
	 *     @type int             $post_parent               Post parent ID to retrieve affiliated comments for.
	 *                                                      Default empty.
	 *     @type string          $search                    Search term(s) to retrieve matching comments for.
	 *                                                      Default empty.
	 *     @type string|array    $status                    Comment statuses to limit results by. Accepts an array
	 *                                                      or space/comma-separated list of 'hold' (`comment_status=0`),
	 *                                                      'approve' (`comment_status=1`), 'all', or a custom
	 *                                                      comment status. Default 'all'.
	 *     @type string|string[] $type                      Include comments of a given type, or array of types.
	 *                                                      Accepts 'comment', 'pings' (includes 'pingback' and
	 *                                                      'trackback'), or any custom type string. Default empty.
	 *     @type string[]        $type__in                  Include comments from a given array of comment types.
	 *                                                      Default empty.
	 *     @type string[]        $type__not_in              Exclude comments from a given array of comment types.
	 *                                                      Default empty.
	 *     @type int             $user_id                   Include comments for a specific user ID. Default empty.
	 *     @type bool|string     $hierarchical              Whether to include comment descendants in the results.
	 *                                                      - 'threaded' returns a tree, with each comment's children
	 *                                                        stored in a `children` property on the `WP_Comment` object.
	 *                                                      - 'flat' returns a flat array of found comments plus
	 *                                                        their children.
	 *                                                      - Boolean `false` leaves out descendants.
	 *                                                      The parameter is ignored (forced to `false`) when
	 *                                                      `$fields` is 'ids' or 'counts'. Accepts 'threaded',
	 *                                                      'flat', or false. Default: false.
	 *     @type string          $cache_domain              Unique cache key to be produced when this query is stored in
	 *                                                      an object cache. Default is 'core'.
	 *     @type bool            $update_comment_meta_cache Whether to prime the metadata cache for found comments.
	 *                                                      Default true.
	 *     @type bool            $update_comment_post_cache Whether to prime the cache for comment posts.
	 *                                                      Default false.
	 * }
	 */
	public function __construct( $query = '' ) {
		$this->query_var_defaults = array(
			'author_email'              => '',
			'author_url'                => '',
			'author__in'                => '',
			'author__not_in'            => '',
			'include_unapproved'        => '',
			'fields'                    => '',
			'ID'                        => '',
			'comment__in'               => '',
			'comment__not_in'           => '',
			'karma'                     => '',
			'number'                    => '',
			'offset'                    => '',
			'no_found_rows'             => true,
			'orderby'                   => '',
			'order'                     => 'DESC',
			'paged'                     => 1,
			'parent'                    => '',
			'parent__in'                => '',
			'parent__not_in'            => '',
			'post_author__in'           => '',
			'post_author__not_in'       => '',
			'post_ID'                   => '',
			'post_id'                   => 0,
			'post__in'                  => '',
			'post__not_in'              => '',
			'post_author'               => '',
			'post_name'                 => '',
			'post_parent'               => '',
			'post_status'               => '',
			'post_type'                 => '',
			'status'                    => 'all',
			'type'                      => '',
			'type__in'                  => '',
			'type__not_in'              => '',
			'user_id'                   => '',
			'search'                    => '',
			'count'                     => false,
			'meta_key'                  => '',
			'meta_value'                => '',
			'meta_query'                => '',
			'date_query'                => null, // See WP_Date_Query.
			'hierarchical'              => false,
			'cache_domain'              => 'core',
			'update_comment_meta_cache' => true,
			'update_comment_post_cache' => false,
		);

		if ( ! empty( $query ) ) {
			$this->query( $query );
		}
	}

	/**
	 * Parse arguments passed to the comment query with default query parameters.
	 *
	 * @since 4.2.0 Extracted from WP_Comment_Query::query().
	 *
	 * @param string|array $query WP_Comment_Query arguments. See WP_Comment_Query::__construct() for accepted arguments.
	 */
	public function parse_query( $query = '' ) {
		if ( empty( $query ) ) {
			$query = $this->query_vars;
		}

		$this->query_vars = wp_parse_args( $query, $this->query_var_defaults );

		/**
		 * Fires after the comment query vars have been parsed.
		 *
		 * @since 4.2.0
		 *
		 * @param WP_Comment_Query $query The WP_Comment_Query instance (passed by reference).
		 */
		do_action_ref_array( 'parse_comment_query', array( &$this ) );
	}

	/**
	 * Sets up the WordPress query for retrieving comments.
	 *
	 * @since 3.1.0
	 * @since 4.1.0 Introduced 'comment__in', 'comment__not_in', 'post_author__in',
	 *              'post_author__not_in', 'author__in', 'author__not_in', 'post__in',
	 *              'post__not_in', 'include_unapproved', 'type__in', and 'type__not_in'
	 *              arguments to $query_vars.
	 * @since 4.2.0 Moved parsing to WP_Comment_Query::parse_query().
	 *
	 * @param string|array $query Array or URL query string of parameters.
	 * @return array|int List of comments, or number of comments when 'count' is passed as a query var.
	 */
	public function query( $query ) {
		$this->query_vars = wp_parse_args( $query );
		return $this->get_comments();
	}

	/**
	 * Get a list of comments matching the query vars.
	 *
	 * @since 4.2.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return int|int[]|WP_Comment[] List of comments or number of found comments if `$count` argument is true.
	 */
	public function get_comments() {
		global $wpdb;

		$this->parse_query();

		// Parse meta query.
		$this->meta_query = new WP_Meta_Query();
		$this->meta_query->parse_query_vars( $this->query_vars );

		/**
		 * Fires before comments are retrieved.
		 *
		 * @since 3.1.0
		 *
		 * @param WP_Comment_Query $query Current instance of WP_Comment_Query (passed by reference).
		 */
		do_action_ref_array( 'pre_get_comments', array( &$this ) );

		// Reparse query vars, in case they were modified in a 'pre_get_comments' callback.
		$this->meta_query->parse_query_vars( $this->query_vars );
		if ( ! empty( $this->meta_query->queries ) ) {
			$this->meta_query_clauses = $this->meta_query->get_sql( 'comment', $wpdb->comments, 'comment_ID', $this );
		}

		$comment_data = null;

		/**
		 * Filters the comments data before the query takes place.
		 *
		 * Return a non-null value to bypass WordPress' default comment queries.
		 *
		 * The expected return type from this filter depends on the value passed
		 * in the request query vars:
		 * - When `$this->query_vars['count']` is set, the filter should return
		 *   the comment count as an integer.
		 * - When `'ids' === $this->query_vars['fields']`, the filter should return
		 *   an array of comment IDs.
		 * - Otherwise the filter should return an array of WP_Comment objects.
		 *
		 * Note that if the filter returns an array of comment data, it will be assigned
		 * to the `comments` property of the current WP_Comment_Query instance.
		 *
		 * Filtering functions that require pagination information are encouraged to set
		 * the `found_comments` and `max_num_pages` properties of the WP_Comment_Query object,
		 * passed to the filter by reference. If WP_Comment_Query does not perform a database
		 * query, it will not have enough information to generate these values itself.
		 *
		 * @since 5.3.0
		 * @since 5.6.0 The returned array of comment data is assigned to the `comments` property
		 *              of the current WP_Comment_Query instance.
		 *
		 * @param array|int|null   $comment_data Return an array of comment data to short-circuit WP's comment query,
		 *                                       the comment count as an integer if `$this->query_vars['count']` is set,
		 *                                       or null to allow WP to run its normal queries.
		 * @param WP_Comment_Query $query        The WP_Comment_Query instance, passed by reference.
		 */
		$comment_data = apply_filters_ref_array( 'comments_pre_query', array( $comment_data, &$this ) );

		if ( null !== $comment_data ) {
			if ( is_array( $comment_data ) && ! $this->query_vars['count'] ) {
				$this->comments = $comment_data;
			}

			return $comment_data;
		}

		/*
		 * Only use the args defined in the query_var_defaults to compute the key,
		 * but ignore 'fields', 'update_comment_meta_cache', 'update_comment_post_cache' which does not affect query results.
		 */
		$_args = wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) );
		unset( $_args['fields'], $_args['update_comment_meta_cache'], $_args['update_comment_post_cache'] );

		$key          = md5( serialize( $_args ) );
		$last_changed = wp_cache_get_last_changed( 'comment' );

		$cache_key   = "get_comments:$key";
		$cache_value = wp_cache_get_salted( $cache_key, 'comment-queries', $last_changed );
		if ( false === $cache_value ) {
			$comment_ids = $this->get_comment_ids();
			if ( $comment_ids ) {
				$this->set_found_comments();
			}

			$cache_value = array(
				'comment_ids'    => $comment_ids,
				'found_comments' => $this->found_comments,
			);
			wp_cache_set_salted( $cache_key, $cache_value, 'comment-queries', $last_changed );
		} else {
			$comment_ids          = $cache_value['comment_ids'];
			$this->found_comments = $cache_value['found_comments'];
		}

		if ( $this->found_comments && $this->query_vars['number'] ) {
			$this->max_num_pages = (int) ceil( $this->found_comments / $this->query_vars['number'] );
		}

		// If querying for a count only, there's nothing more to do.
		if ( $this->query_vars['count'] ) {
			// $comment_ids is actually a count in this case.
			return (int) $comment_ids;
		}

		$comment_ids = array_map( 'intval', $comment_ids );

		if ( $this->query_vars['update_comment_meta_cache'] ) {
			wp_lazyload_comment_meta( $comment_ids );
		}

		if ( 'ids' === $this->query_vars['fields'] ) {
			$this->comments = $comment_ids;
			return $this->comments;
		}

		_prime_comment_caches( $comment_ids, false );

		// Fetch full comment objects from the primed cache.
		$_comments = array();
		foreach ( $comment_ids as $comment_id ) {
			$_comment = get_comment( $comment_id );
			if ( $_comment ) {
				$_comments[] = $_comment;
			}
		}

		// Prime comment post caches.
		if ( $this->query_vars['update_comment_post_cache'] ) {
			$comment_post_ids = array();
			foreach ( $_comments as $_comment ) {
				$comment_post_ids[] = $_comment->comment_post_ID;
			}

			_prime_post_caches( $comment_post_ids, false, false );
		}

		/**
		 * Filters the comment query results.
		 *
		 * @since 3.1.0
		 *
		 * @param WP_Comment[]     $_comments An array of comments.
		 * @param WP_Comment_Query $query     Current instance of WP_Comment_Query (passed by reference).
		 */
		$_comments = apply_filters_ref_array( 'the_comments', array( $_comments, &$this ) );

		// Convert to WP_Comment instances.
		$comments = array_map( 'get_comment', $_comments );

		if ( $this->query_vars['hierarchical'] ) {
			$comments = $this->fill_descendants( $comments );
		}

		$this->comments = $comments;
		return $this->comments;
	}

	/**
	 * Used internally to get a list of comment IDs matching the query vars.
	 *
	 * @since 4.4.0
	 * @since 6.9.0 Excludes the 'note' comment type, unless 'all' or the 'note' types are requested.
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @return int|array A single count of comment IDs if a count query. An array of comment IDs if a full query.
	 */
	protected function get_comment_ids() {
		global $wpdb;

		// Assemble clauses related to 'comment_approved'.
		$approved_clauses = array();

		// 'status' accepts an array or a comma-separated string.
		$status_clauses = array();
		$statuses       = wp_parse_list( $this->query_vars['status'] );

		// Empty 'status' should be interpreted as 'all'.
		if ( empty( $statuses ) ) {
			$statuses = array( 'all' );
		}

		// 'any' overrides other statuses.
		if ( ! in_array( 'any', $statuses, true ) ) {
			foreach ( $statuses as $status ) {
				switch ( $status ) {
					case 'hold':
						$status_clauses[] = "comment_approved = '0'";
						break;

					case 'approve':
						$status_clauses[] = "comment_approved = '1'";
						break;

					case 'all':
					case '':
						$status_clauses[] = "( comment_approved = '0' OR comment_approved = '1' )";
						break;

					default:
						$status_clauses[] = $wpdb->prepare( 'comment_approved = %s', $status );
						break;
				}
			}

			$approved_clauses[] = '( ' . implode( ' OR ', $status_clauses ) . ' )';
		}

		// User IDs or emails whose unapproved comments are included, regardless of $status.
		if ( ! empty( $this->query_vars['include_unapproved'] ) ) {
			$include_unapproved = wp_parse_list( $this->query_vars['include_unapproved'] );

			foreach ( $include_unapproved as $unapproved_identifier ) {
				// Numeric values are assumed to be user IDs.
				if ( is_numeric( $unapproved_identifier ) ) {
					$approved_clauses[] = $wpdb->prepare( "( user_id = %d AND comment_approved = '0' )", $unapproved_identifier );
				} else {
					// Otherwise we match against email addresses.
					if ( ! empty( $_GET['unapproved'] ) && ! empty( $_GET['moderation-hash'] ) ) {
						// Only include requested comment.
						$approved_clauses[] = $wpdb->prepare( "( comment_author_email = %s AND comment_approved = '0' AND {$wpdb->comments}.comment_ID = %d )", $unapproved_identifier, (int) $_GET['unapproved'] );
					} else {
						// Include all of the author's unapproved comments.
						$approved_clauses[] = $wpdb->prepare( "( comment_author_email = %s AND comment_approved = '0' )", $unapproved_identifier );
					}
				}
			}
		}

		// Collapse comment_approved clauses into a single OR-separated clause.
		if ( ! empty( $approved_clauses ) ) {
			if ( 1 === count( $approved_clauses ) ) {
				$this->sql_clauses['where']['approved'] = $approved_clauses[0];
			} else {
				$this->sql_clauses['where']['approved'] = '( ' . implode( ' OR ', $approved_clauses ) . ' )';
			}
		}

		$order = ( 'ASC' === strtoupper( $this->query_vars['order'] ) ) ? 'ASC' : 'DESC';

		// Disable ORDER BY with 'none', an empty array, or boolean false.
		if ( in_array( $this->query_vars['orderby'], array( 'none', array(), false ), true ) ) {
			$orderby = '';
		} elseif ( ! empty( $this->query_vars['orderby'] ) ) {
			$ordersby = is_array( $this->query_vars['orderby'] ) ?
				$this->query_vars['orderby'] :
				preg_split( '/[,\s]/', $this->query_vars['orderby'] );

			$orderby_array            = array();
			$found_orderby_comment_id = false;
			foreach ( $ordersby as $_key => $_value ) {
				if ( ! $_value ) {
					continue;
				}

				if ( is_int( $_key ) ) {
					$_orderby = $_value;
					$_order   = $order;
				} else {
					$_orderby = $_key;
					$_order   = $_value;
				}

				if ( ! $found_orderby_comment_id && in_array( $_orderby, array( 'comment_ID', 'comment__in' ), true ) ) {
					$found_orderby_comment_id = true;
				}

				$parsed = $this->parse_orderby( $_orderby );

				if ( ! $parsed ) {
					continue;
				}

				if ( 'comment__in' === $_orderby ) {
					$orderby_array[] = $parsed;
					continue;
				}

				$orderby_array[] = $parsed . ' ' . $this->parse_order( $_order );
			}

			// If no valid clauses were found, order by comment_date_gmt.
			if ( empty( $orderby_array ) ) {
				$orderby_array[] = "$wpdb->comments.comment_date_gmt $order";
			}

			// To ensure determinate sorting, always include a comment_ID clause.
			if ( ! $found_orderby_comment_id ) {
				$comment_id_order = '';

				// Inherit order from comment_date or comment_date_gmt, if available.
				foreach ( $orderby_array as $orderby_clause ) {
					if ( preg_match( '/comment_date(?:_gmt)*\ (ASC|DESC)/', $orderby_clause, $match ) ) {
						$comment_id_order = $match[1];
						break;
					}
				}

				// If no date-related order is available, use the date from the first available clause.
				if ( ! $comment_id_order ) {
					foreach ( $orderby_array as $orderby_clause ) {
						if ( str_contains( 'ASC', $orderby_clause ) ) {
							$comment_id_order = 'ASC';
						} else {
							$comment_id_order = 'DESC';
						}

						break;
					}
				}

				// Default to DESC.
				if ( ! $comment_id_order ) {
					$comment_id_order = 'DESC';
				}

				$orderby_array[] = "$wpdb->comments.comment_ID $comment_id_order";
			}

			$orderby = implode( ', ', $orderby_array );
		} else {
			$orderby = "$wpdb->comments.comment_date_gmt $order";
		}

		$number = absint( $this->query_vars['number'] );
		$offset = absint( $this->query_vars['offset'] );
		$paged  = absint( $this->query_vars['paged'] );
		$limits = '';

		if ( ! empty( $number ) ) {
			if ( $offset ) {
				$limits = 'LIMIT ' . $offset . ',' . $number;
			} else {
				$limits = 'LIMIT ' . ( $number * ( $paged - 1 ) ) . ',' . $number;
			}
		}

		if ( $this->query_vars['count'] ) {
			$fields = 'COUNT(*)';
		} else {
			$fields = "$wpdb->comments.comment_ID";
		}

		$post_id = absint( $this->query_vars['post_id'] );
		if ( ! empty( $post_id ) ) {
			$this->sql_clauses['where']['post_id'] = $wpdb->prepare( 'comment_post_ID = %d', $post_id );
		}

		// Parse comment IDs for an IN clause.
		if ( ! empty( $this->query_vars['comment__in'] ) ) {
			$this->sql_clauses['where']['comment__in'] = "$wpdb->comments.comment_ID IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__in'] ) ) . ' )';
		}

		// Parse comment IDs for a NOT IN clause.
		if ( ! empty( $this->query_vars['comment__not_in'] ) ) {
			$this->sql_clauses['where']['comment__not_in'] = "$wpdb->comments.comment_ID NOT IN ( " . implode( ',', wp_parse_id_list( $this->query_vars['comment__not_in'] ) ) . ' )';
		}

		// Parse comment parent IDs for an IN clause.
		if ( ! empty( $this->query_vars['parent__in'] ) ) {
			$this->sql_clauses['where']['parent__in'] = 'comment_parent IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__in'] ) ) . ' )';
		}

		// Parse comment parent IDs for a NOT IN clause.
		if ( ! empty( $this->query_vars['parent__not_in'] ) ) {
			$this->sql_clauses['where']['parent__not_in'] = 'comment_parent NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['parent__not_in'] ) ) . ' )';
		}

		// Parse comment post IDs for an IN clause.
		if ( ! empty( $this->query_vars['post__in'] ) ) {
			$this->sql_clauses['where']['post__in'] = 'comment_post_ID IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__in'] ) ) . ' )';
		}

		// Parse comment post IDs for a NOT IN clause.
		if ( ! empty( $this->query_vars['post__not_in'] ) ) {
			$this->sql_clauses['where']['post__not_in'] = 'comment_post_ID NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post__not_in'] ) ) . ' )';
		}

		if ( '' !== $this->query_vars['author_email'] ) {
			$this->sql_clauses['where']['author_email'] = $wpdb->prepare( 'comment_author_email = %s', $this->query_vars['author_email'] );
		}

		if ( '' !== $this->query_vars['author_url'] ) {
			$this->sql_clauses['where']['author_url'] = $wpdb->prepare( 'comment_author_url = %s', $this->query_vars['author_url'] );
		}

		if ( '' !== $this->query_vars['karma'] ) {
			$this->sql_clauses['where']['karma'] = $wpdb->prepare( 'comment_karma = %d', $this->query_vars['karma'] );
		}

		// Filtering by comment_type: 'type', 'type__in', 'type__not_in'.
		$raw_types = array(
			'IN'     => array_merge( (array) $this->query_vars['type'], (array) $this->query_vars['type__in'] ),
			'NOT IN' => (array) $this->query_vars['type__not_in'],
		);

		// Exclude the 'note' comment type, unless 'all' types or the 'note' type explicitly are requested.
		if (
			! in_array( 'all', $raw_types['IN'], true ) &&
			! in_array( 'note', $raw_types['IN'], true ) &&
			! in_array( 'note', $raw_types['NOT IN'], true )
		) {
			$raw_types['NOT IN'][] = 'note';
		}

		$comment_types = array();
		foreach ( $raw_types as $operator => $_raw_types ) {
			$_raw_types = array_unique( $_raw_types );

			foreach ( $_raw_types as $type ) {
				switch ( $type ) {
					// An empty translates to 'all', for backward compatibility.
					case '':
					case 'all':
						break;

					case 'comment':
					case 'comments':
						$comment_types[ $operator ][] = "''";
						$comment_types[ $operator ][] = "'comment'";
						break;

					case 'pings':
						$comment_types[ $operator ][] = "'pingback'";
						$comment_types[ $operator ][] = "'trackback'";
						break;

					default:
						$comment_types[ $operator ][] = $wpdb->prepare( '%s', $type );
						break;
				}
			}

			if ( ! empty( $comment_types[ $operator ] ) ) {
				$types_sql = implode( ', ', $comment_types[ $operator ] );
				$this->sql_clauses['where'][ 'comment_type__' . strtolower( str_replace( ' ', '_', $operator ) ) ] = "comment_type $operator ($types_sql)";
			}
		}

		$parent = $this->query_vars['parent'];
		if ( $this->query_vars['hierarchical'] && ! $parent ) {
			$parent = 0;
		}

		if ( '' !== $parent ) {
			$this->sql_clauses['where']['parent'] = $wpdb->prepare( 'comment_parent = %d', $parent );
		}

		if ( is_array( $this->query_vars['user_id'] ) ) {
			$this->sql_clauses['where']['user_id'] = 'user_id IN (' . implode( ',', array_map( 'absint', $this->query_vars['user_id'] ) ) . ')';
		} elseif ( '' !== $this->query_vars['user_id'] ) {
			$this->sql_clauses['where']['user_id'] = $wpdb->prepare( 'user_id = %d', $this->query_vars['user_id'] );
		}

		// Falsey search strings are ignored.
		if ( isset( $this->query_vars['search'] ) && strlen( $this->query_vars['search'] ) ) {
			$search_sql = $this->get_search_sql(
				$this->query_vars['search'],
				array( 'comment_author', 'comment_author_email', 'comment_author_url', 'comment_author_IP', 'comment_content' )
			);

			// Strip leading 'AND'.
			$this->sql_clauses['where']['search'] = preg_replace( '/^\s*AND\s*/', '', $search_sql );
		}

		// If any post-related query vars are passed, join the posts table.
		$join_posts_table = false;
		$plucked          = wp_array_slice_assoc( $this->query_vars, array( 'post_author', 'post_name', 'post_parent' ) );
		$post_fields      = array_filter( $plucked );

		if ( ! empty( $post_fields ) ) {
			$join_posts_table = true;
			foreach ( $post_fields as $field_name => $field_value ) {
				// $field_value may be an array.
				$esses = array_fill( 0, count( (array) $field_value ), '%s' );

				// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				$this->sql_clauses['where'][ $field_name ] = $wpdb->prepare( " {$wpdb->posts}.{$field_name} IN (" . implode( ',', $esses ) . ')', $field_value );
			}
		}

		// 'post_status' and 'post_type' are handled separately, due to the specialized behavior of 'any'.
		foreach ( array( 'post_status', 'post_type' ) as $field_name ) {
			$q_values = array();
			if ( ! empty( $this->query_vars[ $field_name ] ) ) {
				$q_values = $this->query_vars[ $field_name ];
				if ( ! is_array( $q_values ) ) {
					$q_values = explode( ',', $q_values );
				}

				// 'any' will cause the query var to be ignored.
				if ( in_array( 'any', $q_values, true ) || empty( $q_values ) ) {
					continue;
				}

				$join_posts_table = true;

				$esses = array_fill( 0, count( $q_values ), '%s' );

				// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
				$this->sql_clauses['where'][ $field_name ] = $wpdb->prepare( " {$wpdb->posts}.{$field_name} IN (" . implode( ',', $esses ) . ')', $q_values );
			}
		}

		// Comment author IDs for an IN clause.
		if ( ! empty( $this->query_vars['author__in'] ) ) {
			$this->sql_clauses['where']['author__in'] = 'user_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__in'] ) ) . ' )';
		}

		// Comment author IDs for a NOT IN clause.
		if ( ! empty( $this->query_vars['author__not_in'] ) ) {
			$this->sql_clauses['where']['author__not_in'] = 'user_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['author__not_in'] ) ) . ' )';
		}

		// Post author IDs for an IN clause.
		if ( ! empty( $this->query_vars['post_author__in'] ) ) {
			$join_posts_table                              = true;
			$this->sql_clauses['where']['post_author__in'] = 'post_author IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__in'] ) ) . ' )';
		}

		// Post author IDs for a NOT IN clause.
		if ( ! empty( $this->query_vars['post_author__not_in'] ) ) {
			$join_posts_table                                  = true;
			$this->sql_clauses['where']['post_author__not_in'] = 'post_author NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['post_author__not_in'] ) ) . ' )';
		}

		$join    = '';
		$groupby = '';

		if ( $join_posts_table ) {
			$join .= "JOIN $wpdb->posts ON $wpdb->posts.ID = $wpdb->comments.comment_post_ID";
		}

		if ( ! empty( $this->meta_query_clauses ) ) {
			$join .= $this->meta_query_clauses['join'];

			// Strip leading 'AND'.
			$this->sql_clauses['where']['meta_query'] = preg_replace( '/^\s*AND\s*/', '', $this->meta_query_clauses['where'] );

			if ( ! $this->query_vars['count'] ) {
				$groupby = "{$wpdb->comments}.comment_ID";
			}
		}

		if ( ! empty( $this->query_vars['date_query'] ) && is_array( $this->query_vars['date_query'] ) ) {
			$this->date_query = new WP_Date_Query( $this->query_vars['date_query'], 'comment_date' );

			// Strip leading 'AND'.
			$this->sql_clauses['where']['date_query'] = preg_replace( '/^\s*AND\s*/', '', $this->date_query->get_sql() );
		}

		$where = implode( ' AND ', $this->sql_clauses['where'] );

		$pieces = array( 'fields', 'join', 'where', 'orderby', 'limits', 'groupby' );

		/**
		 * Filters the comment query clauses.
		 *
		 * @since 3.1.0
		 *
		 * @param string[]         $clauses {
		 *     Associative array of the clauses for the query.
		 *
		 *     @type string $fields   The SELECT clause of the query.
		 *     @type string $join     The JOIN clause of the query.
		 *     @type string $where    The WHERE clause of the query.
		 *     @type string $orderby  The ORDER BY clause of the query.
		 *     @type string $limits   The LIMIT clause of the query.
		 *     @type string $groupby  The GROUP BY clause of the query.
		 * }
		 * @param WP_Comment_Query $query   Current instance of WP_Comment_Query (passed by reference).
		 */
		$clauses = apply_filters_ref_array( 'comments_clauses', array( compact( $pieces ), &$this ) );

		$fields  = isset( $clauses['fields'] ) ? $clauses['fields'] : '';
		$join    = isset( $clauses['join'] ) ? $clauses['join'] : '';
		$where   = isset( $clauses['where'] ) ? $clauses['where'] : '';
		$orderby = isset( $clauses['orderby'] ) ? $clauses['orderby'] : '';
		$limits  = isset( $clauses['limits'] ) ? $clauses['limits'] : '';
		$groupby = isset( $clauses['groupby'] ) ? $clauses['groupby'] : '';

		$this->filtered_where_clause = $where;

		if ( $where ) {
			$where = 'WHERE ' . $where;
		}

		if ( $groupby ) {
			$groupby = 'GROUP BY ' . $groupby;
		}

		if ( $orderby ) {
			$orderby = "ORDER BY $orderby";
		}

		$found_rows = '';
		if ( ! $this->query_vars['no_found_rows'] ) {
			$found_rows = 'SQL_CALC_FOUND_ROWS';
		}

		$this->sql_clauses['select']  = "SELECT $found_rows $fields";
		$this->sql_clauses['from']    = "FROM $wpdb->comments $join";
		$this->sql_clauses['groupby'] = $groupby;
		$this->sql_clauses['orderby'] = $orderby;
		$this->sql_clauses['limits']  = $limits;

		// Beginning of the string is on a new line to prevent leading whitespace. See https://core.trac.wordpress.org/ticket/56841.
		$this->request =
			"{$this->sql_clauses['select']}
			 {$this->sql_clauses['from']}
			 {$where}
			 {$this->sql_clauses['groupby']}
			 {$this->sql_clauses['orderby']}
			 {$this->sql_clauses['limits']}";

		if ( $this->query_vars['count'] ) {
			return (int) $wpdb->get_var( $this->request );
		} else {
			$comment_ids = $wpdb->get_col( $this->request );
			return array_map( 'intval', $comment_ids );
		}
	}

	/**
	 * Populates found_comments and max_num_pages properties for the current
	 * query if the limit clause was used.
	 *
	 * @since 4.6.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 */
	private function set_found_comments() {
		global $wpdb;

		if ( $this->query_vars['number'] && ! $this->query_vars['no_found_rows'] ) {
			/**
			 * Filters the query used to retrieve found comment count.
			 *
			 * @since 4.4.0
			 *
			 * @param string           $found_comments_query SQL query. Default 'SELECT FOUND_ROWS()'.
			 * @param WP_Comment_Query $comment_query        The `WP_Comment_Query` instance.
			 */
			$found_comments_query = apply_filters( 'found_comments_query', 'SELECT FOUND_ROWS()', $this );

			$this->found_comments = (int) $wpdb->get_var( $found_comments_query );
		}
	}

	/**
	 * Fetch descendants for located comments.
	 *
	 * Instead of calling `get_children()` separately on each child comment, we do a single set of queries to fetch
	 * the descendant trees for all matched top-level comments.
	 *
	 * @since 4.4.0
	 *
	 * @param WP_Comment[] $comments Array of top-level comments whose descendants should be filled in.
	 * @return array
	 */
	protected function fill_descendants( $comments ) {
		$levels = array(
			0 => wp_list_pluck( $comments, 'comment_ID' ),
		);

		$key          = md5( serialize( wp_array_slice_assoc( $this->query_vars, array_keys( $this->query_var_defaults ) ) ) );
		$last_changed = wp_cache_get_last_changed( 'comment' );

		// Fetch an entire level of the descendant tree at a time.
		$level        = 0;
		$exclude_keys = array( 'parent', 'parent__in', 'parent__not_in' );
		do {
			// Parent-child relationships may be cached. Only query for those that are not.
			$child_ids           = array();
			$uncached_parent_ids = array();
			$_parent_ids         = $levels[ $level ];
			if ( $_parent_ids ) {
				$cache_keys = array();
				foreach ( $_parent_ids as $parent_id ) {
					$cache_keys[ $parent_id ] = "get_comment_child_ids:$parent_id:$key";
				}
				$cache_data = wp_cache_get_multiple_salted( array_values( $cache_keys ), 'comment-queries', $last_changed );
				foreach ( $_parent_ids as $parent_id ) {
					$parent_child_ids = $cache_data[ $cache_keys[ $parent_id ] ];
					if ( false !== $parent_child_ids ) {
						$child_ids = array_merge( $child_ids, $parent_child_ids );
					} else {
						$uncached_parent_ids[] = $parent_id;
					}
				}
			}

			if ( $uncached_parent_ids ) {
				// Fetch this level of comments.
				$parent_query_args = $this->query_vars;
				foreach ( $exclude_keys as $exclude_key ) {
					$parent_query_args[ $exclude_key ] = '';
				}
				$parent_query_args['parent__in']    = $uncached_parent_ids;
				$parent_query_args['no_found_rows'] = true;
				$parent_query_args['hierarchical']  = false;
				$parent_query_args['offset']        = 0;
				$parent_query_args['number']        = 0;

				$level_comments = get_comments( $parent_query_args );

				// Cache parent-child relationships.
				$parent_map = array_fill_keys( $uncached_parent_ids, array() );
				foreach ( $level_comments as $level_comment ) {
					$parent_map[ $level_comment->comment_parent ][] = $level_comment->comment_ID;
					$child_ids[]                                    = $level_comment->comment_ID;
				}

				$data = array();
				foreach ( $parent_map as $parent_id => $children ) {
					$cache_key          = "get_comment_child_ids:$parent_id:$key";
					$data[ $cache_key ] = $children;
				}
				wp_cache_set_multiple_salted( $data, 'comment-queries', $last_changed );
			}

			++$level;
			$levels[ $level ] = $child_ids;
		} while ( $child_ids );

		// Prime comment caches for non-top-level comments.
		$descendant_ids = array();
		for ( $i = 1, $c = count( $levels ); $i < $c; $i++ ) {
			$descendant_ids = array_merge( $descendant_ids, $levels[ $i ] );
		}

		_prime_comment_caches( $descendant_ids, $this->query_vars['update_comment_meta_cache'] );

		// Assemble a flat array of all comments + descendants.
		$all_comments = $comments;
		foreach ( $descendant_ids as $descendant_id ) {
			$all_comments[] = get_comment( $descendant_id );
		}

		// If a threaded representation was requested, build the tree.
		if ( 'threaded' === $this->query_vars['hierarchical'] ) {
			$threaded_comments = array();
			$ref               = array();
			foreach ( $all_comments as $k => $c ) {
				$_c = get_comment( $c->comment_ID );

				// If the comment isn't in the reference array, it goes in the top level of the thread.
				if ( ! isset( $ref[ $c->comment_parent ] ) ) {
					$threaded_comments[ $_c->comment_ID ] = $_c;
					$ref[ $_c->comment_ID ]               = $threaded_comments[ $_c->comment_ID ];

					// Otherwise, set it as a child of its parent.
				} else {

					$ref[ $_c->comment_parent ]->add_child( $_c );
					$ref[ $_c->comment_ID ] = $ref[ $_c->comment_parent ]->get_child( $_c->comment_ID );
				}
			}

			// Set the 'populated_children' flag, to ensure additional database queries aren't run.
			foreach ( $ref as $_ref ) {
				$_ref->populated_children( true );
			}

			$comments = $threaded_comments;
		} else {
			$comments = $all_comments;
		}

		return $comments;
	}

	/**
	 * Used internally to generate an SQL string for searching across multiple columns.
	 *
	 * @since 3.1.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string   $search  Search string.
	 * @param string[] $columns Array of columns to search.
	 * @return string Search SQL.
	 */
	protected function get_search_sql( $search, $columns ) {
		global $wpdb;

		$like = '%' . $wpdb->esc_like( $search ) . '%';

		$searches = array();
		foreach ( $columns as $column ) {
			$searches[] = $wpdb->prepare( "$column LIKE %s", $like );
		}

		return ' AND (' . implode( ' OR ', $searches ) . ')';
	}

	/**
	 * Parse and sanitize 'orderby' keys passed to the comment query.
	 *
	 * @since 4.2.0
	 *
	 * @global wpdb $wpdb WordPress database abstraction object.
	 *
	 * @param string $orderby Alias for the field to order by.
	 * @return string|false Value to used in the ORDER clause. False otherwise.
	 */
	protected function parse_orderby( $orderby ) {
		global $wpdb;

		$allowed_keys = array(
			'comment_agent',
			'comment_approved',
			'comment_author',
			'comment_author_email',
			'comment_author_IP',
			'comment_author_url',
			'comment_content',
			'comment_date',
			'comment_date_gmt',
			'comment_ID',
			'comment_karma',
			'comment_parent',
			'comment_post_ID',
			'comment_type',
			'user_id',
		);

		if ( ! empty( $this->query_vars['meta_key'] ) ) {
			$allowed_keys[] = $this->query_vars['meta_key'];
			$allowed_keys[] = 'meta_value';
			$allowed_keys[] = 'meta_value_num';
		}

		$meta_query_clauses = $this->meta_query->get_clauses();
		if ( $meta_query_clauses ) {
			$allowed_keys = array_merge( $allowed_keys, array_keys( $meta_query_clauses ) );
		}

		$parsed = false;
		if ( $this->query_vars['meta_key'] === $orderby || 'meta_value' === $orderby ) {
			$parsed = "$wpdb->commentmeta.meta_value";
		} elseif ( 'meta_value_num' === $orderby ) {
			$parsed = "$wpdb->commentmeta.meta_value+0";
		} elseif ( 'comment__in' === $orderby ) {
			$comment__in = implode( ',', array_map( 'absint', $this->query_vars['comment__in'] ) );
			$parsed      = "FIELD( {$wpdb->comments}.comment_ID, $comment__in )";
		} elseif ( in_array( $orderby, $allowed_keys, true ) ) {

			if ( isset( $meta_query_clauses[ $orderby ] ) ) {
				$meta_clause = $meta_query_clauses[ $orderby ];
				$parsed      = sprintf( 'CAST(%s.meta_value AS %s)', esc_sql( $meta_clause['alias'] ), esc_sql( $meta_clause['cast'] ) );
			} else {
				$parsed = "$wpdb->comments.$orderby";
			}
		}

		return $parsed;
	}

	/**
	 * Parse an 'order' query variable and cast it to ASC or DESC as necessary.
	 *
	 * @since 4.2.0
	 *
	 * @param string $order The 'order' query variable.
	 * @return string The sanitized 'order' query variable.
	 */
	protected function parse_order( $order ) {
		if ( ! is_string( $order ) || empty( $order ) ) {
			return 'DESC';
		}

		if ( 'ASC' === strtoupper( $order ) ) {
			return 'ASC';
		} else {
			return 'DESC';
		}
	}
}
                                                                                                                                                                                                                                                                                                                                                            ./wp-includes/script-modules.php                                                                    0000644 0001750 0001750 00000023267 15152744170 017426  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Script Modules API: Script Module functions
 *
 * @since 6.5.0
 *
 * @package WordPress
 * @subpackage Script Modules
 */

/**
 * Retrieves the main WP_Script_Modules instance.
 *
 * This function provides access to the WP_Script_Modules instance, creating one
 * if it doesn't exist yet.
 *
 * @since 6.5.0
 *
 * @global WP_Script_Modules $wp_script_modules
 *
 * @return WP_Script_Modules The main WP_Script_Modules instance.
 */
function wp_script_modules(): WP_Script_Modules {
	global $wp_script_modules;

	if ( ! ( $wp_script_modules instanceof WP_Script_Modules ) ) {
		$wp_script_modules = new WP_Script_Modules();
	}

	return $wp_script_modules;
}

/**
 * Registers the script module if no script module with that script module
 * identifier has already been registered.
 *
 * @since 6.5.0
 * @since 6.9.0 Added the $args parameter.
 *
 * @param string            $id      The identifier of the script module. Should be unique. It will be used in the
 *                                   final import map.
 * @param string            $src     Optional. Full URL of the script module, or path of the script module relative
 *                                   to the WordPress root directory. If it is provided and the script module has
 *                                   not been registered yet, it will be registered.
 * @param array             $deps    {
 *                                       Optional. List of dependencies.
 *
 *                                       @type string|array ...$0 {
 *                                           An array of script module identifiers of the dependencies of this script
 *                                           module. The dependencies can be strings or arrays. If they are arrays,
 *                                           they need an `id` key with the script module identifier, and can contain
 *                                           an `import` key with either `static` or `dynamic`. By default,
 *                                           dependencies that don't contain an `import` key are considered static.
 *
 *                                           @type string $id     The script module identifier.
 *                                           @type string $import Optional. Import type. May be either `static` or
 *                                                                `dynamic`. Defaults to `static`.
 *                                       }
 *                                   }
 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
 *                                   It is added to the URL as a query string for cache busting purposes. If $version
 *                                   is set to false, the version number is the currently installed WordPress version.
 *                                   If $version is set to null, no version is added.
 * @param array             $args    {
 *     Optional. An array of additional args. Default empty array.
 *
 *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
 *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
 * }
 */
function wp_register_script_module( string $id, string $src, array $deps = array(), $version = false, array $args = array() ) {
	wp_script_modules()->register( $id, $src, $deps, $version, $args );
}

/**
 * Marks the script module to be enqueued in the page.
 *
 * If a src is provided and the script module has not been registered yet, it
 * will be registered.
 *
 * @since 6.5.0
 * @since 6.9.0 Added the $args parameter.
 *
 * @param string            $id      The identifier of the script module. Should be unique. It will be used in the
 *                                   final import map.
 * @param string            $src     Optional. Full URL of the script module, or path of the script module relative
 *                                   to the WordPress root directory. If it is provided and the script module has
 *                                   not been registered yet, it will be registered.
 * @param array             $deps    {
 *                                       Optional. List of dependencies.
 *
 *                                       @type string|array ...$0 {
 *                                           An array of script module identifiers of the dependencies of this script
 *                                           module. The dependencies can be strings or arrays. If they are arrays,
 *                                           they need an `id` key with the script module identifier, and can contain
 *                                           an `import` key with either `static` or `dynamic`. By default,
 *                                           dependencies that don't contain an `import` key are considered static.
 *
 *                                           @type string $id     The script module identifier.
 *                                           @type string $import Optional. Import type. May be either `static` or
 *                                                                `dynamic`. Defaults to `static`.
 *                                       }
 *                                   }
 * @param string|false|null $version Optional. String specifying the script module version number. Defaults to false.
 *                                   It is added to the URL as a query string for cache busting purposes. If $version
 *                                   is set to false, the version number is the currently installed WordPress version.
 *                                   If $version is set to null, no version is added.
 * @param array             $args    {
 *     Optional. An array of additional args. Default empty array.
 *
 *     @type bool                $in_footer     Whether to print the script module in the footer. Only relevant to block themes. Default 'false'. Optional.
 *     @type 'auto'|'low'|'high' $fetchpriority Fetch priority. Default 'auto'. Optional.
 * }
 */
function wp_enqueue_script_module( string $id, string $src = '', array $deps = array(), $version = false, array $args = array() ) {
	wp_script_modules()->enqueue( $id, $src, $deps, $version, $args );
}

/**
 * Unmarks the script module so it is no longer enqueued in the page.
 *
 * @since 6.5.0
 *
 * @param string $id The identifier of the script module.
 */
function wp_dequeue_script_module( string $id ) {
	wp_script_modules()->dequeue( $id );
}

/**
 * Deregisters the script module.
 *
 * @since 6.5.0
 *
 * @param string $id The identifier of the script module.
 */
function wp_deregister_script_module( string $id ) {
	wp_script_modules()->deregister( $id );
}

/**
 * Registers all the default WordPress Script Modules.
 *
 * @since 6.7.0
 */
function wp_default_script_modules() {
	$suffix = defined( 'WP_RUN_CORE_TESTS' ) ? '.min' : wp_scripts_get_suffix();

	/*
	 * Expects multidimensional array like:
	 *
	 *     'interactivity/index.min.js' => array('dependencies' => array(…), 'version' => '…'),
	 *     'interactivity/debug.min.js' => array('dependencies' => array(…), 'version' => '…'),
	 *     'interactivity-router/index.min.js' => …
	 */
	$assets = include ABSPATH . WPINC . "/assets/script-modules-packages{$suffix}.php";

	foreach ( $assets as $file_name => $script_module_data ) {
		/*
		 * Build the WordPress Script Module ID from the file name.
		 * Prepend `@wordpress/` and remove extensions and `/index` if present:
		 *   - interactivity/index.min.js  => @wordpress/interactivity
		 *   - interactivity/debug.min.js  => @wordpress/interactivity/debug
		 *   - block-library/query/view.js => @wordpress/block-library/query/view
		 */
		$script_module_id = '@wordpress/' . preg_replace( '~(?:/index)?(?:\.min)?\.js$~D', '', $file_name, 1 );

		switch ( $script_module_id ) {
			/*
			 * Interactivity exposes two entrypoints, "/index" and "/debug".
			 * "/debug" should replace "/index" in development.
			 */
			case '@wordpress/interactivity/debug':
				if ( ! SCRIPT_DEBUG ) {
					continue 2;
				}
				$script_module_id = '@wordpress/interactivity';
				break;
			case '@wordpress/interactivity':
				if ( SCRIPT_DEBUG ) {
					continue 2;
				}
				break;
		}

		/*
		 * The Interactivity API is designed with server-side rendering as its primary goal, so all of its script modules
		 * should be loaded with low fetchpriority and printed in the footer since they should not be needed in the
		 * critical rendering path. Also, the @wordpress/a11y script module is intended to be used as a dynamic import
		 * dependency, in which case the fetchpriority is irrelevant. See <https://make.wordpress.org/core/2024/10/14/updates-to-script-modules-in-6-7/>.
		 * However, in case it is added as a static import dependency, the fetchpriority is explicitly set to be 'low'
		 * since the module should not be involved in the critical rendering path, and if it is, its fetchpriority will
		 * be bumped to match the fetchpriority of the dependent script.
		 */
		$args = array();
		if (
			str_starts_with( $script_module_id, '@wordpress/interactivity' ) ||
			str_starts_with( $script_module_id, '@wordpress/block-library' ) ||
			'@wordpress/a11y' === $script_module_id
		) {
			$args['fetchpriority'] = 'low';
			$args['in_footer']     = true;
		}

		// Marks all Core blocks as compatible with client-side navigation.
		if ( str_starts_with( $script_module_id, '@wordpress/block-library' ) ) {
			wp_interactivity()->add_client_navigation_support_to_script_module( $script_module_id );
		}

		$path = includes_url( "js/dist/script-modules/{$file_name}" );
		wp_register_script_module( $script_module_id, $path, $script_module_data['dependencies'], $script_module_data['version'], $args );
	}
}
                                                                                                                                                                                                                                                                                                                                         ./wp-includes/class-wp-customize-control.php                                                        0000644 0001750 0001750 00000063007 15152744170 021677  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * WordPress Customize Control classes
 *
 * @package WordPress
 * @subpackage Customize
 * @since 3.4.0
 */

// Don't load directly.
if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

/**
 * Customize Control class.
 *
 * @since 3.4.0
 */
#[AllowDynamicProperties]
class WP_Customize_Control {

	/**
	 * Incremented with each new class instantiation, then stored in $instance_number.
	 *
	 * Used when sorting two instances whose priorities are equal.
	 *
	 * @since 4.1.0
	 * @var int
	 */
	protected static $instance_count = 0;

	/**
	 * Order in which this instance was created in relation to other instances.
	 *
	 * @since 4.1.0
	 * @var int
	 */
	public $instance_number;

	/**
	 * Customizer manager.
	 *
	 * @since 3.4.0
	 * @var WP_Customize_Manager
	 */
	public $manager;

	/**
	 * Control ID.
	 *
	 * @since 3.4.0
	 * @var string
	 */
	public $id;

	/**
	 * All settings tied to the control.
	 *
	 * @since 3.4.0
	 * @var array
	 */
	public $settings;

	/**
	 * The primary setting for the control (if there is one).
	 *
	 * @since 3.4.0
	 * @var string|WP_Customize_Setting|null
	 */
	public $setting = 'default';

	/**
	 * Capability required to use this control.
	 *
	 * Normally this is empty and the capability is derived from the capabilities
	 * of the associated `$settings`.
	 *
	 * @since 4.5.0
	 * @var string
	 */
	public $capability;

	/**
	 * Order priority to load the control in Customizer.
	 *
	 * @since 3.4.0
	 * @var int
	 */
	public $priority = 10;

	/**
	 * Section the control belongs to.
	 *
	 * @since 3.4.0
	 * @var string
	 */
	public $section = '';

	/**
	 * Label for the control.
	 *
	 * @since 3.4.0
	 * @var string
	 */
	public $label = '';

	/**
	 * Description for the control.
	 *
	 * @since 4.0.0
	 * @var string
	 */
	public $description = '';

	/**
	 * List of choices for 'radio' or 'select' type controls, where values are the keys, and labels are the values.
	 *
	 * @since 3.4.0
	 * @var array
	 */
	public $choices = array();

	/**
	 * List of custom input attributes for control output, where attribute names are the keys and values are the values.
	 *
	 * Not used for 'checkbox', 'radio', 'select', or 'dropdown-pages' control types.
	 *
	 * @since 4.0.0
	 * @var array
	 */
	public $input_attrs = array();

	/**
	 * Show UI for adding new content, currently only used for the dropdown-pages control.
	 *
	 * @since 4.7.0
	 * @var bool
	 */
	public $allow_addition = false;

	/**
	 * @deprecated It is better to just call the json() method
	 * @since 3.4.0
	 * @var array
	 */
	public $json = array();

	/**
	 * Control's Type.
	 *
	 * @since 3.4.0
	 * @var string
	 */
	public $type = 'text';

	/**
	 * Callback.
	 *
	 * @since 4.0.0
	 *
	 * @see WP_Customize_Control::active()
	 *
	 * @var callable Callback is called with one argument, the instance of
	 *               WP_Customize_Control, and returns bool to indicate whether
	 *               the control is active (such as it relates to the URL
	 *               currently being previewed).
	 */
	public $active_callback = '';

	/**
	 * Constructor.
	 *
	 * Supplied `$args` override class property defaults.
	 *
	 * If `$args['settings']` is not defined, use the `$id` as the setting ID.
	 *
	 * @since 3.4.0
	 *
	 * @param WP_Customize_Manager $manager Customizer bootstrap instance.
	 * @param string               $id      Control ID.
	 * @param array                $args    {
	 *     Optional. Array of properties for the new Control object. Default empty array.
	 *
	 *     @type int                  $instance_number Order in which this instance was created in relation
	 *                                                 to other instances.
	 *     @type WP_Customize_Manager $manager         Customizer bootstrap instance.
	 *     @type string               $id              Control ID.
	 *     @type array                $settings        All settings tied to the control. If undefined, `$id` will
	 *                                                 be used.
	 *     @type string               $setting         The primary setting for the control (if there is one).
	 *                                                 Default 'default'.
	 *     @type string               $capability      Capability required to use this control. Normally this is empty
	 *                                                 and the capability is derived from `$settings`.
	 *     @type int                  $priority        Order priority to load the control. Default 10.
	 *     @type string               $section         Section the control belongs to. Default empty.
	 *     @type string               $label           Label for the control. Default empty.
	 *     @type string               $description     Description for the control. Default empty.
	 *     @type array                $choices         List of choices for 'radio' or 'select' type controls, where
	 *                                                 values are the keys, and labels are the values.
	 *                                                 Default empty array.
	 *     @type array                $input_attrs     List of custom input attributes for control output, where
	 *                                                 attribute names are the keys and values are the values. Not
	 *                                                 used for 'checkbox', 'radio', 'select', or 'dropdown-pages'
	 *                                                 control types. Default empty array.
	 *     @type bool                 $allow_addition  Show UI for adding new content, currently only used for the
	 *                                                 dropdown-pages control. Default false.
	 *     @type array                $json            Deprecated. Use WP_Customize_Control::json() instead.
	 *     @type string               $type            Control type. Core controls include 'text', 'checkbox',
	 *                                                 'textarea', 'radio', 'select', and 'dropdown-pages'. Additional
	 *                                                 input types such as 'email', 'url', 'number', 'hidden', and
	 *                                                 'date' are supported implicitly. Default 'text'.
	 *     @type callable             $active_callback Active callback.
	 * }
	 */
	public function __construct( $manager, $id, $args = array() ) {
		$keys = array_keys( get_object_vars( $this ) );
		foreach ( $keys as $key ) {
			if ( isset( $args[ $key ] ) ) {
				$this->$key = $args[ $key ];
			}
		}

		$this->manager = $manager;
		$this->id      = $id;
		if ( empty( $this->active_callback ) ) {
			$this->active_callback = array( $this, 'active_callback' );
		}
		self::$instance_count += 1;
		$this->instance_number = self::$instance_count;

		// Process settings.
		if ( ! isset( $this->settings ) ) {
			$this->settings = $id;
		}

		$settings = array();
		if ( is_array( $this->settings ) ) {
			foreach ( $this->settings as $key => $setting ) {
				$settings[ $key ] = $this->manager->get_setting( $setting );
			}
		} elseif ( is_string( $this->settings ) ) {
			$this->setting       = $this->manager->get_setting( $this->settings );
			$settings['default'] = $this->setting;
		}
		$this->settings = $settings;
	}

	/**
	 * Enqueues control related scripts/styles.
	 *
	 * @since 3.4.0
	 */
	public function enqueue() {}

	/**
	 * Checks whether control is active to current Customizer preview.
	 *
	 * @since 4.0.0
	 *
	 * @return bool Whether the control is active to the current preview.
	 */
	final public function active() {
		$control = $this;
		$active  = call_user_func( $this->active_callback, $this );

		/**
		 * Filters response of WP_Customize_Control::active().
		 *
		 * @since 4.0.0
		 *
		 * @param bool                 $active  Whether the Customizer control is active.
		 * @param WP_Customize_Control $control WP_Customize_Control instance.
		 */
		$active = apply_filters( 'customize_control_active', $active, $control );

		return $active;
	}

	/**
	 * Default callback used when invoking WP_Customize_Control::active().
	 *
	 * Subclasses can override this with their specific logic, or they may
	 * provide an 'active_callback' argument to the constructor.
	 *
	 * @since 4.0.0
	 *
	 * @return true Always true.
	 */
	public function active_callback() {
		return true;
	}

	/**
	 * Fetches a setting's value.
	 * Grabs the main setting by default.
	 *
	 * @since 3.4.0
	 *
	 * @param string $setting_key
	 * @return mixed The requested setting's value, if the setting exists.
	 */
	final public function value( $setting_key = 'default' ) {
		if ( isset( $this->settings[ $setting_key ] ) ) {
			return $this->settings[ $setting_key ]->value();
		}
	}

	/**
	 * Refreshes the parameters passed to the JavaScript via JSON.
	 *
	 * @since 3.4.0
	 */
	public function to_json() {
		$this->json['settings'] = array();
		foreach ( $this->settings as $key => $setting ) {
			$this->json['settings'][ $key ] = $setting->id;
		}

		$this->json['type']           = $this->type;
		$this->json['priority']       = $this->priority;
		$this->json['active']         = $this->active();
		$this->json['section']        = $this->section;
		$this->json['content']        = $this->get_content();
		$this->json['label']          = $this->label;
		$this->json['description']    = $this->description;
		$this->json['instanceNumber'] = $this->instance_number;

		if ( 'dropdown-pages' === $this->type ) {
			$this->json['allow_addition'] = $this->allow_addition;
		}
	}

	/**
	 * Gets the data to export to the client via JSON.
	 *
	 * @since 4.1.0
	 *
	 * @return array Array of parameters passed to the JavaScript.
	 */
	public function json() {
		$this->to_json();
		return $this->json;
	}

	/**
	 * Checks if the user can use this control.
	 *
	 * Returns false if the user cannot manipulate one of the associated settings,
	 * or if one of the associated settings does not exist. Also returns false if
	 * the associated section does not exist or if its capability check returns
	 * false.
	 *
	 * @since 3.4.0
	 *
	 * @return bool False if theme doesn't support the control or user doesn't have the required permissions, otherwise true.
	 */
	final public function check_capabilities() {
		if ( ! empty( $this->capability ) && ! current_user_can( $this->capability ) ) {
			return false;
		}

		foreach ( $this->settings as $setting ) {
			if ( ! $setting || ! $setting->check_capabilities() ) {
				return false;
			}
		}

		$section = $this->manager->get_section( $this->section );
		if ( isset( $section ) && ! $section->check_capabilities() ) {
			return false;
		}

		return true;
	}

	/**
	 * Gets the control's content for insertion into the Customizer pane.
	 *
	 * @since 4.1.0
	 *
	 * @return string Contents of the control.
	 */
	final public function get_content() {
		ob_start();
		$this->maybe_render();
		return trim( ob_get_clean() );
	}

	/**
	 * Checks capabilities and render the control.
	 *
	 * @since 3.4.0
	 * @uses WP_Customize_Control::render()
	 */
	final public function maybe_render() {
		if ( ! $this->check_capabilities() ) {
			return;
		}

		/**
		 * Fires just before the current Customizer control is rendered.
		 *
		 * @since 3.4.0
		 *
		 * @param WP_Customize_Control $control WP_Customize_Control instance.
		 */
		do_action( 'customize_render_control', $this );

		/**
		 * Fires just before a specific Customizer control is rendered.
		 *
		 * The dynamic portion of the hook name, `$this->id`, refers to
		 * the control ID.
		 *
		 * @since 3.4.0
		 *
		 * @param WP_Customize_Control $control WP_Customize_Control instance.
		 */
		do_action( "customize_render_control_{$this->id}", $this );

		$this->render();
	}

	/**
	 * Renders the control wrapper and calls $this->render_content() for the internals.
	 *
	 * @since 3.4.0
	 */
	protected function render() {
		$id    = 'customize-control-' . str_replace( array( '[', ']' ), array( '-', '' ), $this->id );
		$class = 'customize-control customize-control-' . $this->type;

		printf( '<li id="%s" class="%s">', esc_attr( $id ), esc_attr( $class ) );
		$this->render_content();
		echo '</li>';
	}

	/**
	 * Gets the data link attribute for a setting.
	 *
	 * @since 3.4.0
	 * @since 4.9.0 Return a `data-customize-setting-key-link` attribute if a setting is not registered for the supplied setting key.
	 *
	 * @param string $setting_key
	 * @return string Data link parameter, a `data-customize-setting-link` attribute if the `$setting_key` refers
	 *                to a pre-registered setting, and a `data-customize-setting-key-link` attribute if the setting
	 *                is not yet registered.
	 */
	public function get_link( $setting_key = 'default' ) {
		if ( isset( $this->settings[ $setting_key ] ) && $this->settings[ $setting_key ] instanceof WP_Customize_Setting ) {
			return 'data-customize-setting-link="' . esc_attr( $this->settings[ $setting_key ]->id ) . '"';
		} else {
			return 'data-customize-setting-key-link="' . esc_attr( $setting_key ) . '"';
		}
	}

	/**
	 * Renders the data link attribute for the control's input element.
	 *
	 * @since 3.4.0
	 * @uses WP_Customize_Control::get_link()
	 *
	 * @param string $setting_key Default 'default'.
	 */
	public function link( $setting_key = 'default' ) {
		echo $this->get_link( $setting_key );
	}

	/**
	 * Renders the custom attributes for the control's input element.
	 *
	 * @since 4.0.0
	 */
	public function input_attrs() {
		foreach ( $this->input_attrs as $attr => $value ) {
			echo $attr . '="' . esc_attr( $value ) . '" ';
		}
	}

	/**
	 * Renders the control's content.
	 *
	 * Allows the content to be overridden without having to rewrite the wrapper in `$this::render()`.
	 *
	 * Supports basic input types `text`, `checkbox`, `textarea`, `radio`, `select` and `dropdown-pages`.
	 * Additional input types such as `email`, `url`, `number`, `hidden` and `date` are supported implicitly.
	 *
	 * Control content can alternately be rendered in JS. See WP_Customize_Control::print_template().
	 *
	 * @since 3.4.0
	 */
	protected function render_content() {
		$input_id         = '_customize-input-' . $this->id;
		$description_id   = '_customize-description-' . $this->id;
		$describedby_attr = ( ! empty( $this->description ) ) ? ' aria-describedby="' . esc_attr( $description_id ) . '" ' : '';
		switch ( $this->type ) {
			case 'checkbox':
				?>
				<span class="customize-inside-control-row">
					<input
						id="<?php echo esc_attr( $input_id ); ?>"
						<?php echo $describedby_attr; ?>
						type="checkbox"
						value="<?php echo esc_attr( $this->value() ); ?>"
						<?php $this->link(); ?>
						<?php checked( $this->value() ); ?>
					/>
					<label for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $this->label ); ?></label>
					<?php if ( ! empty( $this->description ) ) : ?>
						<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
					<?php endif; ?>
				</span>
				<?php
				break;
			case 'radio':
				if ( empty( $this->choices ) ) {
					return;
				}

				$name = '_customize-radio-' . $this->id;
				?>
				<?php if ( ! empty( $this->label ) ) : ?>
					<span class="customize-control-title"><?php echo esc_html( $this->label ); ?></span>
				<?php endif; ?>
				<?php if ( ! empty( $this->description ) ) : ?>
					<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
				<?php endif; ?>

				<?php foreach ( $this->choices as $value => $label ) : ?>
					<span class="customize-inside-control-row">
						<input
							id="<?php echo esc_attr( $input_id . '-radio-' . $value ); ?>"
							type="radio"
							<?php echo $describedby_attr; ?>
							value="<?php echo esc_attr( $value ); ?>"
							name="<?php echo esc_attr( $name ); ?>"
							<?php $this->link(); ?>
							<?php checked( $this->value(), $value ); ?>
							/>
						<label for="<?php echo esc_attr( $input_id . '-radio-' . $value ); ?>"><?php echo esc_html( $label ); ?></label>
					</span>
				<?php endforeach; ?>
				<?php
				break;
			case 'select':
				if ( empty( $this->choices ) ) {
					return;
				}

				?>
				<?php if ( ! empty( $this->label ) ) : ?>
					<label for="<?php echo esc_attr( $input_id ); ?>" class="customize-control-title"><?php echo esc_html( $this->label ); ?></label>
				<?php endif; ?>
				<?php if ( ! empty( $this->description ) ) : ?>
					<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
				<?php endif; ?>

				<select id="<?php echo esc_attr( $input_id ); ?>" <?php echo $describedby_attr; ?> <?php $this->link(); ?>>
					<?php
					foreach ( $this->choices as $value => $label ) {
						echo '<option value="' . esc_attr( $value ) . '"' . selected( $this->value(), $value, false ) . '>' . esc_html( $label ) . '</option>';
					}
					?>
				</select>
				<?php
				break;
			case 'textarea':
				if ( ! array_key_exists( 'rows', $this->input_attrs ) ) {
					$this->input_attrs['rows'] = 5;
				}
				?>
				<?php if ( ! empty( $this->label ) ) : ?>
					<label for="<?php echo esc_attr( $input_id ); ?>" class="customize-control-title"><?php echo esc_html( $this->label ); ?></label>
				<?php endif; ?>
				<?php if ( ! empty( $this->description ) ) : ?>
					<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
				<?php endif; ?>
				<textarea
					id="<?php echo esc_attr( $input_id ); ?>"
					<?php echo $describedby_attr; ?>
					<?php $this->input_attrs(); ?>
					<?php $this->link(); ?>
				><?php echo esc_textarea( $this->value() ); ?></textarea>
				<?php
				break;
			case 'dropdown-pages':
				?>
				<?php if ( ! empty( $this->label ) ) : ?>
					<label for="<?php echo esc_attr( $input_id ); ?>" class="customize-control-title"><?php echo esc_html( $this->label ); ?></label>
				<?php endif; ?>
				<?php if ( ! empty( $this->description ) ) : ?>
					<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
				<?php endif; ?>

				<?php
				$dropdown_name     = '_customize-dropdown-pages-' . $this->id;
				$show_option_none  = __( '&mdash; Select &mdash;' );
				$option_none_value = '0';
				$dropdown          = wp_dropdown_pages(
					array(
						'name'              => $dropdown_name,
						'echo'              => 0,
						'show_option_none'  => $show_option_none,
						'option_none_value' => $option_none_value,
						'selected'          => $this->value(),
					)
				);
				if ( empty( $dropdown ) ) {
					$dropdown  = sprintf( '<select id="%1$s" name="%1$s">', esc_attr( $dropdown_name ) );
					$dropdown .= sprintf( '<option value="%1$s">%2$s</option>', esc_attr( $option_none_value ), esc_html( $show_option_none ) );
					$dropdown .= '</select>';
				}

				// Hackily add in the data link parameter.
				$dropdown = str_replace( '<select', '<select ' . $this->get_link() . ' id="' . esc_attr( $input_id ) . '" ' . $describedby_attr, $dropdown );

				/*
				 * Even more hackily add auto-draft page stubs.
				 * @todo Eventually this should be removed in favor of the pages being injected into the underlying get_pages() call.
				 * See <https://github.com/xwp/wp-customize-posts/pull/250>.
				 */
				$nav_menus_created_posts_setting = $this->manager->get_setting( 'nav_menus_created_posts' );
				if ( $nav_menus_created_posts_setting && current_user_can( 'publish_pages' ) ) {
					$auto_draft_page_options = '';
					foreach ( $nav_menus_created_posts_setting->value() as $auto_draft_page_id ) {
						$post = get_post( $auto_draft_page_id );
						if ( $post && 'page' === $post->post_type ) {
							$auto_draft_page_options .= sprintf( '<option value="%1$s">%2$s</option>', esc_attr( $post->ID ), esc_html( $post->post_title ) );
						}
					}
					if ( $auto_draft_page_options ) {
						$dropdown = str_replace( '</select>', $auto_draft_page_options . '</select>', $dropdown );
					}
				}

				echo $dropdown;
				?>
				<?php if ( $this->allow_addition && current_user_can( 'publish_pages' ) && current_user_can( 'edit_theme_options' ) ) : // Currently tied to menus functionality. ?>
					<button type="button" class="button-link add-new-toggle">
						<?php
						/* translators: %s: Add Page label. */
						printf( __( '+ %s' ), get_post_type_object( 'page' )->labels->add_new_item );
						?>
					</button>
					<div class="new-content-item-wrapper">
						<label for="create-input-<?php echo esc_attr( $this->id ); ?>"><?php _e( 'New page title' ); ?></label>
						<div class="new-content-item">
							<input type="text" id="create-input-<?php echo esc_attr( $this->id ); ?>" class="create-item-input form-required">
							<button type="button" class="button add-content"><?php _e( 'Add' ); ?></button>
						</div>
						<span id="create-input-<?php echo esc_attr( $this->id ); ?>-error" class="create-item-error error-message" style="display: none;"><?php _e( 'Please enter a page title' ); ?></span>

					</div>
				<?php endif; ?>
				<?php
				break;
			default:
				?>
				<?php if ( ! empty( $this->label ) ) : ?>
					<label for="<?php echo esc_attr( $input_id ); ?>" class="customize-control-title"><?php echo esc_html( $this->label ); ?></label>
				<?php endif; ?>
				<?php if ( ! empty( $this->description ) ) : ?>
					<span id="<?php echo esc_attr( $description_id ); ?>" class="description customize-control-description"><?php echo $this->description; ?></span>
				<?php endif; ?>
				<input
					id="<?php echo esc_attr( $input_id ); ?>"
					type="<?php echo esc_attr( $this->type ); ?>"
					<?php echo $describedby_attr; ?>
					<?php $this->input_attrs(); ?>
					<?php if ( ! isset( $this->input_attrs['value'] ) ) : ?>
						value="<?php echo esc_attr( $this->value() ); ?>"
					<?php endif; ?>
					<?php $this->link(); ?>
					/>
				<?php
				break;
		}
	}

	/**
	 * Renders the control's JS template.
	 *
	 * This function is only run for control types that have been registered with
	 * WP_Customize_Manager::register_control_type().
	 *
	 * In the future, this will also print the template for the control's container
	 * element and be override-able.
	 *
	 * @since 4.1.0
	 */
	final public function print_template() {
		?>
		<script type="text/html" id="tmpl-customize-control-<?php echo esc_attr( $this->type ); ?>-content">
			<?php $this->content_template(); ?>
		</script>
		<?php
	}

	/**
	 * An Underscore (JS) template for this control's content (but not its container).
	 *
	 * Class variables for this control class are available in the `data` JS object;
	 * export custom variables by overriding WP_Customize_Control::to_json().
	 *
	 * @see WP_Customize_Control::print_template()
	 *
	 * @since 4.1.0
	 */
	protected function content_template() {}
}

/**
 * WP_Customize_Color_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-color-control.php';

/**
 * WP_Customize_Media_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-media-control.php';

/**
 * WP_Customize_Upload_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-upload-control.php';

/**
 * WP_Customize_Image_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-image-control.php';

/**
 * WP_Customize_Background_Image_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-image-control.php';

/**
 * WP_Customize_Background_Position_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-background-position-control.php';

/**
 * WP_Customize_Cropped_Image_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-cropped-image-control.php';

/**
 * WP_Customize_Site_Icon_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-site-icon-control.php';

/**
 * WP_Customize_Header_Image_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-header-image-control.php';

/**
 * WP_Customize_Theme_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-theme-control.php';

/**
 * WP_Widget_Area_Customize_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-widget-area-customize-control.php';

/**
 * WP_Widget_Form_Customize_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-widget-form-customize-control.php';

/**
 * WP_Customize_Nav_Menu_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-control.php';

/**
 * WP_Customize_Nav_Menu_Item_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-control.php';

/**
 * WP_Customize_Nav_Menu_Location_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-location-control.php';

/**
 * WP_Customize_Nav_Menu_Name_Control class.
 *
 * As this file is deprecated, it will trigger a deprecation notice if instantiated. In a subsequent
 * release, the require_once here will be removed and _deprecated_file() will be called if file is
 * required at all.
 *
 * @deprecated 4.9.0 This file is no longer used due to new menu creation UX.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-name-control.php';

/**
 * WP_Customize_Nav_Menu_Locations_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-locations-control.php';

/**
 * WP_Customize_Nav_Menu_Auto_Add_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-auto-add-control.php';

/**
 * WP_Customize_Date_Time_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-customize-date-time-control.php';

/**
 * WP_Sidebar_Block_Editor_Control class.
 */
require_once ABSPATH . WPINC . '/customize/class-wp-sidebar-block-editor-control.php';
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         ./wp-includes/class-walker-category.php                                                             0000644 0001750 0001750 00000020435 15152744165 020655  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Taxonomy API: Walker_Category class
 *
 * @package WordPress
 * @subpackage Template
 * @since 4.4.0
 */

/**
 * Core class used to create an HTML list of categories.
 *
 * @since 2.1.0
 *
 * @see Walker
 */
class Walker_Category extends Walker {

	/**
	 * What the class handles.
	 *
	 * @since 2.1.0
	 * @var string
	 *
	 * @see Walker::$tree_type
	 */
	public $tree_type = 'category';

	/**
	 * Database fields to use.
	 *
	 * @since 2.1.0
	 * @var string[]
	 *
	 * @see Walker::$db_fields
	 * @todo Decouple this
	 */
	public $db_fields = array(
		'parent' => 'parent',
		'id'     => 'term_id',
	);

	/**
	 * Starts the list before the elements are added.
	 *
	 * @since 2.1.0
	 *
	 * @see Walker::start_lvl()
	 *
	 * @param string $output Used to append additional content. Passed by reference.
	 * @param int    $depth  Optional. Depth of category. Used for tab indentation. Default 0.
	 * @param array  $args   Optional. An array of arguments. Will only append content if style argument
	 *                       value is 'list'. See wp_list_categories(). Default empty array.
	 */
	public function start_lvl( &$output, $depth = 0, $args = array() ) {
		if ( 'list' !== $args['style'] ) {
			return;
		}

		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent<ul class='children'>\n";
	}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * @since 2.1.0
	 *
	 * @see Walker::end_lvl()
	 *
	 * @param string $output Used to append additional content. Passed by reference.
	 * @param int    $depth  Optional. Depth of category. Used for tab indentation. Default 0.
	 * @param array  $args   Optional. An array of arguments. Will only append content if style argument
	 *                       value is 'list'. See wp_list_categories(). Default empty array.
	 */
	public function end_lvl( &$output, $depth = 0, $args = array() ) {
		if ( 'list' !== $args['style'] ) {
			return;
		}

		$indent  = str_repeat( "\t", $depth );
		$output .= "$indent</ul>\n";
	}

	/**
	 * Starts the element output.
	 *
	 * @since 2.1.0
	 * @since 5.9.0 Renamed `$category` to `$data_object` and `$id` to `$current_object_id`
	 *              to match parent class for PHP 8 named parameter support.
	 *
	 * @see Walker::start_el()
	 *
	 * @param string  $output            Used to append additional content (passed by reference).
	 * @param WP_Term $data_object       Category data object.
	 * @param int     $depth             Optional. Depth of category in reference to parents. Default 0.
	 * @param array   $args              Optional. An array of arguments. See wp_list_categories().
	 *                                   Default empty array.
	 * @param int     $current_object_id Optional. ID of the current category. Default 0.
	 */
	public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {
		// Restores the more descriptive, specific name for use within this method.
		$category = $data_object;

		/** This filter is documented in wp-includes/category-template.php */
		$cat_name = apply_filters( 'list_cats', esc_attr( $category->name ), $category );

		// Don't generate an element if the category name is empty.
		if ( '' === $cat_name ) {
			return;
		}

		$atts         = array();
		$atts['href'] = get_term_link( $category );

		if ( $args['use_desc_for_title'] && ! empty( $category->description ) ) {
			/**
			 * Filters the category description for display.
			 *
			 * @since 1.2.0
			 *
			 * @param string  $description Category description.
			 * @param WP_Term $category    Category object.
			 */
			$atts['title'] = strip_tags( apply_filters( 'category_description', $category->description, $category ) );
		}

		/**
		 * Filters the HTML attributes applied to a category list item's anchor element.
		 *
		 * @since 5.2.0
		 *
		 * @param array   $atts {
		 *     The HTML attributes applied to the list item's `<a>` element, empty strings are ignored.
		 *
		 *     @type string $href  The href attribute.
		 *     @type string $title The title attribute.
		 * }
		 * @param WP_Term $category          Term data object.
		 * @param int     $depth             Depth of category, used for padding.
		 * @param array   $args              An array of arguments.
		 * @param int     $current_object_id ID of the current category.
		 */
		$atts = apply_filters( 'category_list_link_attributes', $atts, $category, $depth, $args, $current_object_id );

		$attributes = '';
		foreach ( $atts as $attr => $value ) {
			if ( is_scalar( $value ) && '' !== $value && false !== $value ) {
				$value       = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
				$attributes .= ' ' . $attr . '="' . $value . '"';
			}
		}

		$link = sprintf(
			'<a%s>%s</a>',
			$attributes,
			$cat_name
		);

		if ( ! empty( $args['feed_image'] ) || ! empty( $args['feed'] ) ) {
			$link .= ' ';

			if ( empty( $args['feed_image'] ) ) {
				$link .= '(';
			}

			$link .= '<a href="' . esc_url( get_term_feed_link( $category, $category->taxonomy, $args['feed_type'] ) ) . '"';

			if ( empty( $args['feed'] ) ) {
				/* translators: %s: Category name. */
				$alt = ' alt="' . sprintf( __( 'Feed for all posts filed under %s' ), $cat_name ) . '"';
			} else {
				$alt   = ' alt="' . $args['feed'] . '"';
				$name  = $args['feed'];
				$link .= empty( $args['title'] ) ? '' : $args['title'];
			}

			$link .= '>';

			if ( empty( $args['feed_image'] ) ) {
				$link .= $name;
			} else {
				$link .= "<img src='" . esc_url( $args['feed_image'] ) . "'$alt" . ' />';
			}

			$link .= '</a>';

			if ( empty( $args['feed_image'] ) ) {
				$link .= ')';
			}
		}

		if ( ! empty( $args['show_count'] ) ) {
			$link .= ' (' . number_format_i18n( $category->count ) . ')';
		}

		if ( 'list' === $args['style'] ) {
			$output     .= "\t<li";
			$css_classes = array(
				'cat-item',
				'cat-item-' . $category->term_id,
			);

			if ( ! empty( $args['current_category'] ) ) {
				// 'current_category' can be an array, so we use `get_terms()`.
				$_current_terms = get_terms(
					array(
						'taxonomy'   => $category->taxonomy,
						'include'    => $args['current_category'],
						'hide_empty' => false,
					)
				);

				foreach ( $_current_terms as $_current_term ) {
					if ( $category->term_id === $_current_term->term_id ) {
						$css_classes[] = 'current-cat';
						$link          = str_replace( '<a', '<a aria-current="page"', $link );
					} elseif ( $category->term_id === $_current_term->parent ) {
						$css_classes[] = 'current-cat-parent';
					}

					while ( $_current_term->parent ) {
						if ( $category->term_id === $_current_term->parent ) {
							$css_classes[] = 'current-cat-ancestor';
							break;
						}

						$_current_term = get_term( $_current_term->parent, $category->taxonomy );
					}
				}
			}

			/**
			 * Filters the list of CSS classes to include with each category in the list.
			 *
			 * @since 4.2.0
			 *
			 * @see wp_list_categories()
			 *
			 * @param string[] $css_classes An array of CSS classes to be applied to each list item.
			 * @param WP_Term  $category    Category data object.
			 * @param int      $depth       Depth of page, used for padding.
			 * @param array    $args        An array of wp_list_categories() arguments.
			 */
			$css_classes = implode( ' ', apply_filters( 'category_css_class', $css_classes, $category, $depth, $args ) );
			$css_classes = $css_classes ? ' class="' . esc_attr( $css_classes ) . '"' : '';

			$output .= $css_classes;
			$output .= ">$link\n";
		} elseif ( isset( $args['separator'] ) ) {
			$output .= "\t$link" . $args['separator'] . "\n";
		} else {
			$output .= "\t$link<br />\n";
		}
	}

	/**
	 * Ends the element output, if needed.
	 *
	 * @since 2.1.0
	 * @since 5.9.0 Renamed `$page` to `$data_object` to match parent class for PHP 8 named parameter support.
	 *
	 * @see Walker::end_el()
	 *
	 * @param string $output      Used to append additional content (passed by reference).
	 * @param object $data_object Category data object. Not used.
	 * @param int    $depth       Optional. Depth of category. Not used.
	 * @param array  $args        Optional. An array of arguments. Only uses 'list' for whether should
	 *                            append to output. See wp_list_categories(). Default empty array.
	 */
	public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {
		if ( 'list' !== $args['style'] ) {
			return;
		}

		$output .= "</li>\n";
	}
}
                                                                                                                                                                                                                                   ./wp-includes/style-engine.php                                                                      0000644 0001750 0001750 00000016613 15152744165 017060  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Style engine: Public functions
 *
 * This file contains a variety of public functions developers can use to interact with
 * the Style Engine API.
 *
 * @package WordPress
 * @subpackage StyleEngine
 * @since 6.1.0
 */

/**
 * Global public interface method to generate styles from a single style object,
 * e.g. the value of a block's attributes.style object or the top level styles in theme.json.
 *
 * Example usage:
 *
 *     $styles = wp_style_engine_get_styles(
 *         array(
 *             'color' => array( 'text' => '#cccccc' ),
 *         )
 *     );
 *
 * Returns:
 *
 *     array(
 *         'css'          => 'color: #cccccc',
 *         'declarations' => array( 'color' => '#cccccc' ),
 *         'classnames'   => 'has-color',
 *     )
 *
 * @since 6.1.0
 *
 * @see https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/#styles
 * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/
 *
 * @param array $block_styles The style object.
 * @param array $options {
 *     Optional. An array of options. Default empty array.
 *
 *     @type string|null $context                    An identifier describing the origin of the style object,
 *                                                   e.g. 'block-supports' or 'global-styles'. Default null.
 *                                                   When set, the style engine will attempt to store the CSS rules,
 *                                                   where a selector is also passed.
 *     @type bool        $convert_vars_to_classnames Whether to skip converting incoming CSS var patterns,
 *                                                   e.g. `var:preset|<PRESET_TYPE>|<PRESET_SLUG>`,
 *                                                   to `var( --wp--preset--* )` values. Default false.
 *     @type string      $selector                   Optional. When a selector is passed,
 *                                                   the value of `$css` in the return value will comprise
 *                                                   a full CSS rule `$selector { ...$css_declarations }`,
 *                                                   otherwise, the value will be a concatenated string
 *                                                   of CSS declarations.
 * }
 * @return array {
 *     @type string   $css          A CSS ruleset or declarations block
 *                                  formatted to be placed in an HTML `style` attribute or tag.
 *     @type string[] $declarations An associative array of CSS definitions,
 *                                  e.g. `array( "$property" => "$value", "$property" => "$value" )`.
 *     @type string   $classnames   Classnames separated by a space.
 * }
 */
function wp_style_engine_get_styles( $block_styles, $options = array() ) {
	$options = wp_parse_args(
		$options,
		array(
			'selector'                   => null,
			'context'                    => null,
			'convert_vars_to_classnames' => false,
		)
	);

	$parsed_styles = WP_Style_Engine::parse_block_styles( $block_styles, $options );

	// Output.
	$styles_output = array();

	if ( ! empty( $parsed_styles['declarations'] ) ) {
		$styles_output['css']          = WP_Style_Engine::compile_css( $parsed_styles['declarations'], $options['selector'] );
		$styles_output['declarations'] = $parsed_styles['declarations'];
		if ( ! empty( $options['context'] ) ) {
			WP_Style_Engine::store_css_rule( $options['context'], $options['selector'], $parsed_styles['declarations'] );
		}
	}

	if ( ! empty( $parsed_styles['classnames'] ) ) {
		$styles_output['classnames'] = implode( ' ', array_unique( $parsed_styles['classnames'] ) );
	}

	return array_filter( $styles_output );
}

/**
 * Returns compiled CSS from a collection of selectors and declarations.
 * Useful for returning a compiled stylesheet from any collection of CSS selector + declarations.
 *
 * Example usage:
 *
 *     $css_rules = array(
 *         array(
 *             'selector'     => '.elephant-are-cool',
 *             'declarations' => array(
 *                 'color' => 'gray',
 *                 'width' => '3em',
 *             ),
 *         ),
 *     );
 *
 *     $css = wp_style_engine_get_stylesheet_from_css_rules( $css_rules );
 *
 * Returns:
 *
 *     .elephant-are-cool{color:gray;width:3em}
 *
 * @since 6.1.0
 * @since 6.6.0 Added support for `$rules_group` in the `$css_rules` array.
 *
 * @param array $css_rules {
 *     Required. A collection of CSS rules.
 *
 *     @type array ...$0 {
 *         @type string   $rules_group  A parent CSS selector in the case of nested CSS,
 *                                      or a CSS nested @rule, such as `@media (min-width: 80rem)` or `@layer module`.
 *         @type string   $selector     A CSS selector.
 *         @type string[] $declarations An associative array of CSS definitions,
 *                                      e.g. `array( "$property" => "$value", "$property" => "$value" )`.
 *     }
 * }
 * @param array $options {
 *     Optional. An array of options. Default empty array.
 *
 *     @type string|null $context  An identifier describing the origin of the style object,
 *                                 e.g. 'block-supports' or 'global-styles'. Default 'block-supports'.
 *                                 When set, the style engine will attempt to store the CSS rules.
 *     @type bool        $optimize Whether to optimize the CSS output, e.g. combine rules.
 *                                 Default false.
 *     @type bool        $prettify Whether to add new lines and indents to output.
 *                                 Defaults to whether the `SCRIPT_DEBUG` constant is defined.
 * }
 * @return string A string of compiled CSS declarations, or empty string.
 */
function wp_style_engine_get_stylesheet_from_css_rules( $css_rules, $options = array() ) {
	if ( empty( $css_rules ) ) {
		return '';
	}

	$options = wp_parse_args(
		$options,
		array(
			'context' => null,
		)
	);

	$css_rule_objects = array();
	foreach ( $css_rules as $css_rule ) {
		if ( empty( $css_rule['selector'] ) || empty( $css_rule['declarations'] ) || ! is_array( $css_rule['declarations'] ) ) {
			continue;
		}

		$rules_group = $css_rule['rules_group'] ?? null;
		if ( ! empty( $options['context'] ) ) {
			WP_Style_Engine::store_css_rule( $options['context'], $css_rule['selector'], $css_rule['declarations'], $rules_group );
		}

		$css_rule_objects[] = new WP_Style_Engine_CSS_Rule( $css_rule['selector'], $css_rule['declarations'], $rules_group );
	}

	if ( empty( $css_rule_objects ) ) {
		return '';
	}

	return WP_Style_Engine::compile_stylesheet_from_css_rules( $css_rule_objects, $options );
}

/**
 * Returns compiled CSS from a store, if found.
 *
 * @since 6.1.0
 *
 * @param string $context A valid context name, corresponding to an existing store key.
 * @param array  $options {
 *     Optional. An array of options. Default empty array.
 *
 *     @type bool $optimize Whether to optimize the CSS output, e.g. combine rules.
 *                          Default false.
 *     @type bool $prettify Whether to add new lines and indents to output.
 *                          Defaults to whether the `SCRIPT_DEBUG` constant is defined.
 * }
 * @return string A compiled CSS string.
 */
function wp_style_engine_get_stylesheet_from_context( $context, $options = array() ) {
	return WP_Style_Engine::compile_stylesheet_from_css_rules( WP_Style_Engine::get_store( $context )->get_all_rules(), $options );
}
                                                                                                                     ./wp-includes/nav-menu-template.php                                                                 0000644 0001750 0001750 00000062606 15152744176 020021  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Nav Menu API: Template functions
 *
 * @package WordPress
 * @subpackage Nav_Menus
 * @since 3.0.0
 */

// Don't load directly.
if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

/** Walker_Nav_Menu class */
require_once ABSPATH . WPINC . '/class-walker-nav-menu.php';

/**
 * Displays a navigation menu.
 *
 * @since 3.0.0
 * @since 4.7.0 Added the `item_spacing` argument.
 * @since 5.5.0 Added the `container_aria_label` argument.
 *
 * @param array $args {
 *     Optional. Array of nav menu arguments.
 *
 *     @type int|string|WP_Term $menu                 Desired menu. Accepts a menu ID, slug, name, or object.
 *                                                    Default empty.
 *     @type string             $menu_class           CSS class to use for the ul element which forms the menu.
 *                                                    Default 'menu'.
 *     @type string             $menu_id              The ID that is applied to the ul element which forms the menu.
 *                                                    Default is the menu slug, incremented.
 *     @type string             $container            Whether to wrap the ul, and what to wrap it with.
 *                                                    Default 'div'.
 *     @type string             $container_class      Class that is applied to the container.
 *                                                    Default 'menu-{menu slug}-container'.
 *     @type string             $container_id         The ID that is applied to the container. Default empty.
 *     @type string             $container_aria_label The aria-label attribute that is applied to the container
 *                                                    when it's a nav element. Default empty.
 *     @type callable|false     $fallback_cb          If the menu doesn't exist, a callback function will fire.
 *                                                    Default is 'wp_page_menu'. Set to false for no fallback.
 *     @type string             $before               Text before the link markup. Default empty.
 *     @type string             $after                Text after the link markup. Default empty.
 *     @type string             $link_before          Text before the link text. Default empty.
 *     @type string             $link_after           Text after the link text. Default empty.
 *     @type bool               $echo                 Whether to echo the menu or return it. Default true.
 *     @type int                $depth                How many levels of the hierarchy are to be included.
 *                                                    0 means all. Default 0.
 *                                                    Default 0.
 *     @type object             $walker               Instance of a custom walker class. Default empty.
 *     @type string             $theme_location       Theme location to be used. Must be registered with
 *                                                    register_nav_menu() in order to be selectable by the user.
 *     @type string             $items_wrap           How the list items should be wrapped. Uses printf() format with
 *                                                    numbered placeholders. Default is a ul with an id and class.
 *     @type string             $item_spacing         Whether to preserve whitespace within the menu's HTML.
 *                                                    Accepts 'preserve' or 'discard'. Default 'preserve'.
 * }
 * @return void|string|false Void if 'echo' argument is true, menu output if 'echo' is false.
 *                           False if there are no items or no menu was found.
 */
function wp_nav_menu( $args = array() ) {
	static $menu_id_slugs = array();

	$defaults = array(
		'menu'                 => '',
		'container'            => 'div',
		'container_class'      => '',
		'container_id'         => '',
		'container_aria_label' => '',
		'menu_class'           => 'menu',
		'menu_id'              => '',
		'echo'                 => true,
		'fallback_cb'          => 'wp_page_menu',
		'before'               => '',
		'after'                => '',
		'link_before'          => '',
		'link_after'           => '',
		'items_wrap'           => '<ul id="%1$s" class="%2$s">%3$s</ul>',
		'item_spacing'         => 'preserve',
		'depth'                => 0,
		'walker'               => '',
		'theme_location'       => '',
	);

	$args = wp_parse_args( $args, $defaults );

	if ( ! in_array( $args['item_spacing'], array( 'preserve', 'discard' ), true ) ) {
		// Invalid value, fall back to default.
		$args['item_spacing'] = $defaults['item_spacing'];
	}

	/**
	 * Filters the arguments used to display a navigation menu.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param array $args Array of wp_nav_menu() arguments.
	 */
	$args = apply_filters( 'wp_nav_menu_args', $args );
	$args = (object) $args;

	/**
	 * Filters whether to short-circuit the wp_nav_menu() output.
	 *
	 * Returning a non-null value from the filter will short-circuit wp_nav_menu(),
	 * echoing that value if $args->echo is true, returning that value otherwise.
	 *
	 * @since 3.9.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string|null $output Nav menu output to short-circuit with. Default null.
	 * @param stdClass    $args   An object containing wp_nav_menu() arguments.
	 */
	$nav_menu = apply_filters( 'pre_wp_nav_menu', null, $args );

	if ( null !== $nav_menu ) {
		if ( $args->echo ) {
			echo $nav_menu;
			return;
		}

		return $nav_menu;
	}

	// Get the nav menu based on the requested menu.
	$menu = wp_get_nav_menu_object( $args->menu );

	// Get the nav menu based on the theme_location.
	$locations = get_nav_menu_locations();
	if ( ! $menu && $args->theme_location && $locations && isset( $locations[ $args->theme_location ] ) ) {
		$menu = wp_get_nav_menu_object( $locations[ $args->theme_location ] );
	}

	// Get the first menu that has items if we still can't find a menu.
	if ( ! $menu && ! $args->theme_location ) {
		$menus = wp_get_nav_menus();
		foreach ( $menus as $menu_maybe ) {
			$menu_items = wp_get_nav_menu_items( $menu_maybe->term_id, array( 'update_post_term_cache' => false ) );
			if ( $menu_items ) {
				$menu = $menu_maybe;
				break;
			}
		}
	}

	if ( empty( $args->menu ) ) {
		$args->menu = $menu;
	}

	// If the menu exists, get its items.
	if ( $menu && ! is_wp_error( $menu ) && ! isset( $menu_items ) ) {
		$menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
	}

	/*
	 * If no menu was found:
	 *  - Fall back (if one was specified), or bail.
	 *
	 * If no menu items were found:
	 *  - Fall back, but only if no theme location was specified.
	 *  - Otherwise, bail.
	 */
	if ( ( ! $menu || is_wp_error( $menu ) || ( isset( $menu_items ) && empty( $menu_items ) && ! $args->theme_location ) )
		&& isset( $args->fallback_cb ) && $args->fallback_cb && is_callable( $args->fallback_cb ) ) {
			return call_user_func( $args->fallback_cb, (array) $args );
	}

	if ( ! $menu || is_wp_error( $menu ) ) {
		return false;
	}

	$nav_menu = '';
	$items    = '';

	$show_container = false;
	if ( $args->container ) {
		/**
		 * Filters the list of HTML tags that are valid for use as menu containers.
		 *
		 * @since 3.0.0
		 *
		 * @param string[] $tags The acceptable HTML tags for use as menu containers.
		 *                       Default is array containing 'div' and 'nav'.
		 */
		$allowed_tags = apply_filters( 'wp_nav_menu_container_allowedtags', array( 'div', 'nav' ) );

		if ( is_string( $args->container ) && in_array( $args->container, $allowed_tags, true ) ) {
			$show_container = true;
			$class          = $args->container_class ? ' class="' . esc_attr( $args->container_class ) . '"' : ' class="menu-' . $menu->slug . '-container"';
			$id             = $args->container_id ? ' id="' . esc_attr( $args->container_id ) . '"' : '';
			$aria_label     = ( 'nav' === $args->container && $args->container_aria_label ) ? ' aria-label="' . esc_attr( $args->container_aria_label ) . '"' : '';
			$nav_menu      .= '<' . $args->container . $id . $class . $aria_label . '>';
		}
	}

	// Set up the $menu_item variables.
	_wp_menu_item_classes_by_context( $menu_items );

	$sorted_menu_items        = array();
	$menu_items_with_children = array();
	foreach ( (array) $menu_items as $menu_item ) {
		/*
		 * Fix invalid `menu_item_parent`. See: https://core.trac.wordpress.org/ticket/56926.
		 * Compare as strings. Plugins may change the ID to a string.
		 */
		if ( (string) $menu_item->ID === (string) $menu_item->menu_item_parent ) {
			$menu_item->menu_item_parent = 0;
		}

		$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
		if ( $menu_item->menu_item_parent ) {
			$menu_items_with_children[ $menu_item->menu_item_parent ] = true;
		}
	}

	// Add the menu-item-has-children class where applicable.
	if ( $menu_items_with_children ) {
		foreach ( $sorted_menu_items as &$menu_item ) {
			if ( isset( $menu_items_with_children[ $menu_item->ID ] ) ) {
				$menu_item->classes[] = 'menu-item-has-children';
			}
		}
	}

	unset( $menu_items, $menu_item );

	/**
	 * Filters the sorted list of menu item objects before generating the menu's HTML.
	 *
	 * @since 3.1.0
	 *
	 * @param array    $sorted_menu_items The menu items, sorted by each menu item's menu order.
	 * @param stdClass $args              An object containing wp_nav_menu() arguments.
	 */
	$sorted_menu_items = apply_filters( 'wp_nav_menu_objects', $sorted_menu_items, $args );

	$items .= walk_nav_menu_tree( $sorted_menu_items, $args->depth, $args );
	unset( $sorted_menu_items );

	// Attributes.
	if ( ! empty( $args->menu_id ) ) {
		$wrap_id = $args->menu_id;
	} else {
		$wrap_id = 'menu-' . $menu->slug;

		while ( in_array( $wrap_id, $menu_id_slugs, true ) ) {
			if ( preg_match( '#-(\d+)$#', $wrap_id, $matches ) ) {
				$wrap_id = preg_replace( '#-(\d+)$#', '-' . ++$matches[1], $wrap_id );
			} else {
				$wrap_id = $wrap_id . '-1';
			}
		}
	}
	$menu_id_slugs[] = $wrap_id;

	$wrap_class = $args->menu_class ? $args->menu_class : '';

	/**
	 * Filters the HTML list content for navigation menus.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $items The HTML list content for the menu items.
	 * @param stdClass $args  An object containing wp_nav_menu() arguments.
	 */
	$items = apply_filters( 'wp_nav_menu_items', $items, $args );
	/**
	 * Filters the HTML list content for a specific navigation menu.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $items The HTML list content for the menu items.
	 * @param stdClass $args  An object containing wp_nav_menu() arguments.
	 */
	$items = apply_filters( "wp_nav_menu_{$menu->slug}_items", $items, $args );

	// Don't print any markup if there are no items at this point.
	if ( empty( $items ) ) {
		return false;
	}

	$nav_menu .= sprintf( $args->items_wrap, esc_attr( $wrap_id ), esc_attr( $wrap_class ), $items );
	unset( $items );

	if ( $show_container ) {
		$nav_menu .= '</' . $args->container . '>';
	}

	/**
	 * Filters the HTML content for navigation menus.
	 *
	 * @since 3.0.0
	 *
	 * @see wp_nav_menu()
	 *
	 * @param string   $nav_menu The HTML content for the navigation menu.
	 * @param stdClass $args     An object containing wp_nav_menu() arguments.
	 */
	$nav_menu = apply_filters( 'wp_nav_menu', $nav_menu, $args );

	if ( $args->echo ) {
		echo $nav_menu;
	} else {
		return $nav_menu;
	}
}

/**
 * Adds the class property classes for the current context, if applicable.
 *
 * @access private
 * @since 3.0.0
 *
 * @global WP_Query   $wp_query   WordPress Query object.
 * @global WP_Rewrite $wp_rewrite WordPress rewrite component.
 *
 * @param array $menu_items The current menu item objects to which to add the class property information.
 */
function _wp_menu_item_classes_by_context( &$menu_items ) {
	global $wp_query, $wp_rewrite;

	$queried_object    = $wp_query->get_queried_object();
	$queried_object_id = (int) $wp_query->queried_object_id;

	$active_object               = '';
	$active_ancestor_item_ids    = array();
	$active_parent_item_ids      = array();
	$active_parent_object_ids    = array();
	$possible_taxonomy_ancestors = array();
	$possible_object_parents     = array();
	$home_page_id                = (int) get_option( 'page_for_posts' );

	if ( $wp_query->is_singular && ! empty( $queried_object->post_type ) && ! is_post_type_hierarchical( $queried_object->post_type ) ) {
		foreach ( (array) get_object_taxonomies( $queried_object->post_type ) as $taxonomy ) {
			if ( is_taxonomy_hierarchical( $taxonomy ) ) {
				$term_hierarchy = _get_term_hierarchy( $taxonomy );
				$terms          = wp_get_object_terms( $queried_object_id, $taxonomy, array( 'fields' => 'ids' ) );
				if ( is_array( $terms ) ) {
					$possible_object_parents = array_merge( $possible_object_parents, $terms );
					$term_to_ancestor        = array();
					foreach ( (array) $term_hierarchy as $ancestor => $descendents ) {
						foreach ( (array) $descendents as $desc ) {
							$term_to_ancestor[ $desc ] = $ancestor;
						}
					}

					foreach ( $terms as $desc ) {
						do {
							$possible_taxonomy_ancestors[ $taxonomy ][] = $desc;
							if ( isset( $term_to_ancestor[ $desc ] ) ) {
								$_desc = $term_to_ancestor[ $desc ];
								unset( $term_to_ancestor[ $desc ] );
								$desc = $_desc;
							} else {
								$desc = 0;
							}
						} while ( ! empty( $desc ) );
					}
				}
			}
		}
	} elseif ( ! empty( $queried_object->taxonomy ) && is_taxonomy_hierarchical( $queried_object->taxonomy ) ) {
		$term_hierarchy   = _get_term_hierarchy( $queried_object->taxonomy );
		$term_to_ancestor = array();
		foreach ( (array) $term_hierarchy as $ancestor => $descendents ) {
			foreach ( (array) $descendents as $desc ) {
				$term_to_ancestor[ $desc ] = $ancestor;
			}
		}
		$desc = $queried_object->term_id;
		do {
			$possible_taxonomy_ancestors[ $queried_object->taxonomy ][] = $desc;
			if ( isset( $term_to_ancestor[ $desc ] ) ) {
				$_desc = $term_to_ancestor[ $desc ];
				unset( $term_to_ancestor[ $desc ] );
				$desc = $_desc;
			} else {
				$desc = 0;
			}
		} while ( ! empty( $desc ) );
	}

	$possible_object_parents = array_filter( $possible_object_parents );

	$front_page_url         = home_url();
	$front_page_id          = (int) get_option( 'page_on_front' );
	$privacy_policy_page_id = (int) get_option( 'wp_page_for_privacy_policy' );

	foreach ( (array) $menu_items as $key => $menu_item ) {

		$menu_items[ $key ]->current = false;

		$classes   = (array) $menu_item->classes;
		$classes[] = 'menu-item';
		$classes[] = 'menu-item-type-' . $menu_item->type;
		$classes[] = 'menu-item-object-' . $menu_item->object;

		// This menu item is set as the 'Front Page'.
		if ( 'post_type' === $menu_item->type && $front_page_id === (int) $menu_item->object_id ) {
			$classes[] = 'menu-item-home';
		}

		// This menu item is set as the 'Privacy Policy Page'.
		if ( 'post_type' === $menu_item->type && $privacy_policy_page_id === (int) $menu_item->object_id ) {
			$classes[] = 'menu-item-privacy-policy';
		}

		// If the menu item corresponds to a taxonomy term for the currently queried non-hierarchical post object.
		if ( $wp_query->is_singular && 'taxonomy' === $menu_item->type
			&& in_array( (int) $menu_item->object_id, $possible_object_parents, true )
		) {
			$active_parent_object_ids[] = (int) $menu_item->object_id;
			$active_parent_item_ids[]   = (int) $menu_item->db_id;
			$active_object              = $queried_object->post_type;

			// If the menu item corresponds to the currently queried post or taxonomy object.
		} elseif (
			(int) $menu_item->object_id === $queried_object_id
			&& (
				( ! empty( $home_page_id ) && 'post_type' === $menu_item->type
					&& $wp_query->is_home && $home_page_id === (int) $menu_item->object_id )
				|| ( 'post_type' === $menu_item->type && $wp_query->is_singular )
				|| ( 'taxonomy' === $menu_item->type
					&& ( $wp_query->is_category || $wp_query->is_tag || $wp_query->is_tax )
					&& $queried_object->taxonomy === $menu_item->object )
			)
		) {
			$classes[]                   = 'current-menu-item';
			$menu_items[ $key ]->current = true;
			$ancestor_id                 = (int) $menu_item->db_id;

			while (
				( $ancestor_id = (int) get_post_meta( $ancestor_id, '_menu_item_menu_item_parent', true ) )
				&& ! in_array( $ancestor_id, $active_ancestor_item_ids, true )
			) {
				$active_ancestor_item_ids[] = $ancestor_id;
			}

			if ( 'post_type' === $menu_item->type && 'page' === $menu_item->object ) {
				// Back compat classes for pages to match wp_page_menu().
				$classes[] = 'page_item';
				$classes[] = 'page-item-' . $menu_item->object_id;
				$classes[] = 'current_page_item';
			}

			$active_parent_item_ids[]   = (int) $menu_item->menu_item_parent;
			$active_parent_object_ids[] = (int) $menu_item->post_parent;
			$active_object              = $menu_item->object;

			// If the menu item corresponds to the currently queried post type archive.
		} elseif (
			'post_type_archive' === $menu_item->type
			&& is_post_type_archive( array( $menu_item->object ) )
		) {
			$classes[]                   = 'current-menu-item';
			$menu_items[ $key ]->current = true;
			$ancestor_id                 = (int) $menu_item->db_id;

			while (
				( $ancestor_id = (int) get_post_meta( $ancestor_id, '_menu_item_menu_item_parent', true ) )
				&& ! in_array( $ancestor_id, $active_ancestor_item_ids, true )
			) {
				$active_ancestor_item_ids[] = $ancestor_id;
			}

			$active_parent_item_ids[] = (int) $menu_item->menu_item_parent;

			// If the menu item corresponds to the currently requested URL.
		} elseif ( 'custom' === $menu_item->object && isset( $_SERVER['HTTP_HOST'] ) ) {
			$_root_relative_current = untrailingslashit( $_SERVER['REQUEST_URI'] );

			// If it's the customize page then it will strip the query var off the URL before entering the comparison block.
			if ( is_customize_preview() ) {
				$_root_relative_current = strtok( untrailingslashit( $_SERVER['REQUEST_URI'] ), '?' );
			}

			$current_url        = set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_root_relative_current );
			$raw_item_url       = strpos( $menu_item->url, '#' ) ? substr( $menu_item->url, 0, strpos( $menu_item->url, '#' ) ) : $menu_item->url;
			$item_url           = set_url_scheme( untrailingslashit( $raw_item_url ) );
			$_indexless_current = untrailingslashit( preg_replace( '/' . preg_quote( $wp_rewrite->index, '/' ) . '$/', '', $current_url ) );

			$matches = array(
				$current_url,
				urldecode( $current_url ),
				$_indexless_current,
				urldecode( $_indexless_current ),
				$_root_relative_current,
				urldecode( $_root_relative_current ),
			);

			if ( $raw_item_url && in_array( $item_url, $matches, true ) ) {
				$classes[]                   = 'current-menu-item';
				$menu_items[ $key ]->current = true;
				$ancestor_id                 = (int) $menu_item->db_id;

				while (
					( $ancestor_id = (int) get_post_meta( $ancestor_id, '_menu_item_menu_item_parent', true ) )
					&& ! in_array( $ancestor_id, $active_ancestor_item_ids, true )
				) {
					$active_ancestor_item_ids[] = $ancestor_id;
				}

				if ( in_array( home_url(), array( untrailingslashit( $current_url ), untrailingslashit( $_indexless_current ) ), true ) ) {
					// Back compat for home link to match wp_page_menu().
					$classes[] = 'current_page_item';
				}
				$active_parent_item_ids[]   = (int) $menu_item->menu_item_parent;
				$active_parent_object_ids[] = (int) $menu_item->post_parent;
				$active_object              = $menu_item->object;

				// Give front page item the 'current-menu-item' class when extra query arguments are involved.
			} elseif ( $item_url === $front_page_url && is_front_page() ) {
				$classes[] = 'current-menu-item';
			}

			if ( untrailingslashit( $item_url ) === home_url() ) {
				$classes[] = 'menu-item-home';
			}
		}

		// Back-compat with wp_page_menu(): add "current_page_parent" to static home page link for any non-page query.
		if ( ! empty( $home_page_id ) && 'post_type' === $menu_item->type
			&& empty( $wp_query->is_page ) && $home_page_id === (int) $menu_item->object_id
		) {
			$classes[] = 'current_page_parent';
		}

		$menu_items[ $key ]->classes = array_unique( $classes );
	}
	$active_ancestor_item_ids = array_filter( array_unique( $active_ancestor_item_ids ) );
	$active_parent_item_ids   = array_filter( array_unique( $active_parent_item_ids ) );
	$active_parent_object_ids = array_filter( array_unique( $active_parent_object_ids ) );

	// Set parent's class.
	foreach ( (array) $menu_items as $key => $parent_item ) {
		$classes                                   = (array) $parent_item->classes;
		$menu_items[ $key ]->current_item_ancestor = false;
		$menu_items[ $key ]->current_item_parent   = false;

		if (
			isset( $parent_item->type )
			&& (
				// Ancestral post object.
				(
					'post_type' === $parent_item->type
					&& ! empty( $queried_object->post_type )
					&& is_post_type_hierarchical( $queried_object->post_type )
					&& in_array( (int) $parent_item->object_id, $queried_object->ancestors, true )
					&& (int) $parent_item->object_id !== $queried_object->ID
				) ||

				// Ancestral term.
				(
					'taxonomy' === $parent_item->type
					&& isset( $possible_taxonomy_ancestors[ $parent_item->object ] )
					&& in_array( (int) $parent_item->object_id, $possible_taxonomy_ancestors[ $parent_item->object ], true )
					&& (
						! isset( $queried_object->term_id ) ||
						(int) $parent_item->object_id !== $queried_object->term_id
					)
				)
			)
		) {
			if ( ! empty( $queried_object->taxonomy ) ) {
				$classes[] = 'current-' . $queried_object->taxonomy . '-ancestor';
			} else {
				$classes[] = 'current-' . $queried_object->post_type . '-ancestor';
			}
		}

		if ( in_array( (int) $parent_item->db_id, $active_ancestor_item_ids, true ) ) {
			$classes[] = 'current-menu-ancestor';

			$menu_items[ $key ]->current_item_ancestor = true;
		}
		if ( in_array( (int) $parent_item->db_id, $active_parent_item_ids, true ) ) {
			$classes[] = 'current-menu-parent';

			$menu_items[ $key ]->current_item_parent = true;
		}
		if ( in_array( (int) $parent_item->object_id, $active_parent_object_ids, true ) ) {
			$classes[] = 'current-' . $active_object . '-parent';
		}

		if ( 'post_type' === $parent_item->type && 'page' === $parent_item->object ) {
			// Back compat classes for pages to match wp_page_menu().
			if ( in_array( 'current-menu-parent', $classes, true ) ) {
				$classes[] = 'current_page_parent';
			}
			if ( in_array( 'current-menu-ancestor', $classes, true ) ) {
				$classes[] = 'current_page_ancestor';
			}
		}

		$menu_items[ $key ]->classes = array_unique( $classes );
	}
}

/**
 * Retrieves the HTML list content for nav menu items.
 *
 * @uses Walker_Nav_Menu to create HTML list content.
 * @since 3.0.0
 *
 * @param array    $items The menu items, sorted by each menu item's menu order.
 * @param int      $depth Depth of the item in reference to parents.
 * @param stdClass $args  An object containing wp_nav_menu() arguments.
 * @return string The HTML list content for the menu items.
 */
function walk_nav_menu_tree( $items, $depth, $args ) {
	$walker = ( empty( $args->walker ) ) ? new Walker_Nav_Menu() : $args->walker;

	return $walker->walk( $items, $depth, $args );
}

/**
 * Prevents a menu item ID from being used more than once.
 *
 * @since 3.0.1
 * @access private
 *
 * @param string $id
 * @param object $item
 * @return string
 */
function _nav_menu_item_id_use_once( $id, $item ) {
	static $_used_ids = array();

	if ( in_array( $item->ID, $_used_ids, true ) ) {
		return '';
	}

	$_used_ids[] = $item->ID;

	return $id;
}

/**
 * Remove the `menu-item-has-children` class from bottom level menu items.
 *
 * This runs on the {@see 'nav_menu_css_class'} filter. The $args and $depth
 * parameters were added after the filter was originally introduced in
 * WordPress 3.0.0 so this needs to allow for cases in which the filter is
 * called without them.
 *
 * @see https://core.trac.wordpress.org/ticket/56926
 *
 * @since 6.2.0
 *
 * @param string[]       $classes   Array of the CSS classes that are applied to the menu item's `<li>` element.
 * @param WP_Post        $menu_item The current menu item object.
 * @param stdClass|false $args      An object of wp_nav_menu() arguments. Default false ($args unspecified when filter is called).
 * @param int|false      $depth     Depth of menu item. Default false ($depth unspecified when filter is called).
 * @return string[] Modified nav menu classes.
 */
function wp_nav_menu_remove_menu_item_has_children_class( $classes, $menu_item, $args = false, $depth = false ) {
	/*
	 * Account for the filter being called without the $args or $depth parameters.
	 *
	 * This occurs when a theme uses a custom walker calling the `nav_menu_css_class`
	 * filter using the legacy formats prior to the introduction of the $args and
	 * $depth parameters.
	 *
	 * As both of these parameters are required for this function to determine
	 * both the current and maximum depth of the menu tree, the function does not
	 * attempt to remove the `menu-item-has-children` class if these parameters
	 * are not set.
	 */
	if ( false === $depth || false === $args ) {
		return $classes;
	}

	// Max-depth is 1-based.
	$max_depth = isset( $args->depth ) ? (int) $args->depth : 0;
	// Depth is 0-based so needs to be increased by one.
	$depth = $depth + 1;

	// Complete menu tree is displayed.
	if ( 0 === $max_depth ) {
		return $classes;
	}

	/*
	 * Remove the `menu-item-has-children` class from bottom level menu items.
	 * -1 is used to display all menu items in one level so the class should
	 * be removed from all menu items.
	 */
	if ( -1 === $max_depth || $depth >= $max_depth ) {
		$classes = array_diff( $classes, array( 'menu-item-has-children' ) );
	}

	return $classes;
}
                                                                                                                          ./wp-includes/block-template.php                                                                    0000644 0001750 0001750 00000035777 15152744176 017376  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Block template loader functions.
 *
 * @package WordPress
 */

/**
 * Adds necessary hooks to resolve '_wp-find-template' requests.
 *
 * @access private
 * @since 5.9.0
 */
function _add_template_loader_filters() {
	if ( isset( $_GET['_wp-find-template'] ) && current_theme_supports( 'block-templates' ) ) {
		add_action( 'pre_get_posts', '_resolve_template_for_new_post' );
	}
}

/**
 * Renders a warning screen for empty block templates.
 *
 * @since 6.8.0
 *
 * @param WP_Block_Template $block_template The block template object.
 * @return string The warning screen HTML.
 */
function wp_render_empty_block_template_warning( $block_template ) {
	wp_enqueue_style( 'wp-empty-template-alert' );
	return sprintf(
		/* translators: %1$s: Block template title. %2$s: Empty template warning message. %3$s: Edit template link. %4$s: Edit template button label. */
		'<div id="wp-empty-template-alert">
			<h2>%1$s</h2>
			<p>%2$s</p>
			<a href="%3$s" class="wp-element-button">
				%4$s
			</a>
		</div>',
		esc_html( $block_template->title ),
		__( 'This page is blank because the template is empty. You can reset or customize it in the Site Editor.' ),
		get_edit_post_link( $block_template->wp_id, 'site-editor' ),
		__( 'Edit template' )
	);
}

/**
 * Finds a block template with equal or higher specificity than a given PHP template file.
 *
 * Internally, this communicates the block content that needs to be used by the template canvas through a global variable.
 *
 * @since 5.8.0
 * @since 6.3.0 Added `$_wp_current_template_id` global for editing of current template directly from the admin bar.
 *
 * @global string $_wp_current_template_content
 * @global string $_wp_current_template_id
 *
 * @param string   $template  Path to the template. See locate_template().
 * @param string   $type      Sanitized filename without extension.
 * @param string[] $templates A list of template candidates, in descending order of priority.
 * @return string The path to the Site Editor template canvas file, or the fallback PHP template.
 */
function locate_block_template( $template, $type, array $templates ) {
	global $_wp_current_template_content, $_wp_current_template_id;

	if ( ! current_theme_supports( 'block-templates' ) ) {
		return $template;
	}

	if ( $template ) {
		/*
		 * locate_template() has found a PHP template at the path specified by $template.
		 * That means that we have a fallback candidate if we cannot find a block template
		 * with higher specificity.
		 *
		 * Thus, before looking for matching block themes, we shorten our list of candidate
		 * templates accordingly.
		 */

		// Locate the index of $template (without the theme directory path) in $templates.
		$relative_template_path = str_replace(
			array( get_stylesheet_directory() . '/', get_template_directory() . '/' ),
			'',
			$template
		);
		$index                  = array_search( $relative_template_path, $templates, true );

		// If the template hierarchy algorithm has successfully located a PHP template file,
		// we will only consider block templates with higher or equal specificity.
		$templates = array_slice( $templates, 0, $index + 1 );
	}

	$block_template = resolve_block_template( $type, $templates, $template );

	if ( $block_template ) {
		$_wp_current_template_id = $block_template->id;

		if ( empty( $block_template->content ) ) {
			if ( is_user_logged_in() ) {
				$_wp_current_template_content = wp_render_empty_block_template_warning( $block_template );
			} else {
				if ( $block_template->has_theme_file ) {
					// Show contents from theme template if user is not logged in.
					$theme_template               = _get_block_template_file( 'wp_template', $block_template->slug );
					$_wp_current_template_content = file_get_contents( $theme_template['path'] );
				} else {
					$_wp_current_template_content = $block_template->content;
				}
			}
		} elseif ( ! empty( $block_template->content ) ) {
			$_wp_current_template_content = $block_template->content;
		}
		if ( isset( $_GET['_wp-find-template'] ) ) {
			wp_send_json_success( $block_template );
		}
	} else {
		if ( $template ) {
			return $template;
		}

		if ( 'index' === $type ) {
			if ( isset( $_GET['_wp-find-template'] ) ) {
				wp_send_json_error( array( 'message' => __( 'No matching template found.' ) ) );
			}
		} else {
			return ''; // So that the template loader keeps looking for templates.
		}
	}

	// Add hooks for template canvas.
	// Add viewport meta tag.
	add_action( 'wp_head', '_block_template_viewport_meta_tag', 0 );

	// Render title tag with content, regardless of whether theme has title-tag support.
	remove_action( 'wp_head', '_wp_render_title_tag', 1 );    // Remove conditional title tag rendering...
	add_action( 'wp_head', '_block_template_render_title_tag', 1 ); // ...and make it unconditional.

	// This file will be included instead of the theme's template file.
	return ABSPATH . WPINC . '/template-canvas.php';
}

/**
 * Returns the correct 'wp_template' to render for the request template type.
 *
 * @access private
 * @since 5.8.0
 * @since 5.9.0 Added the `$fallback_template` parameter.
 *
 * @param string   $template_type      The current template type.
 * @param string[] $template_hierarchy The current template hierarchy, ordered by priority.
 * @param string   $fallback_template  A PHP fallback template to use if no matching block template is found.
 * @return WP_Block_Template|null template A template object, or null if none could be found.
 */
function resolve_block_template( $template_type, $template_hierarchy, $fallback_template ) {
	if ( ! $template_type ) {
		return null;
	}

	if ( empty( $template_hierarchy ) ) {
		$template_hierarchy = array( $template_type );
	}

	$slugs = array_map(
		'_strip_template_file_suffix',
		$template_hierarchy
	);

	// Find all potential templates 'wp_template' post matching the hierarchy.
	$query     = array(
		'slug__in' => $slugs,
	);
	$templates = get_block_templates( $query );

	// Order these templates per slug priority.
	// Build map of template slugs to their priority in the current hierarchy.
	$slug_priorities = array_flip( $slugs );

	usort(
		$templates,
		static function ( $template_a, $template_b ) use ( $slug_priorities ) {
			return $slug_priorities[ $template_a->slug ] - $slug_priorities[ $template_b->slug ];
		}
	);

	$theme_base_path        = get_stylesheet_directory() . DIRECTORY_SEPARATOR;
	$parent_theme_base_path = get_template_directory() . DIRECTORY_SEPARATOR;

	// Is the active theme a child theme, and is the PHP fallback template part of it?
	if (
		str_starts_with( $fallback_template, $theme_base_path ) &&
		! str_contains( $fallback_template, $parent_theme_base_path )
	) {
		$fallback_template_slug = substr(
			$fallback_template,
			// Starting position of slug.
			strpos( $fallback_template, $theme_base_path ) + strlen( $theme_base_path ),
			// Remove '.php' suffix.
			-4
		);

		// Is our candidate block template's slug identical to our PHP fallback template's?
		if (
			count( $templates ) &&
			$fallback_template_slug === $templates[0]->slug &&
			'theme' === $templates[0]->source
		) {
			// Unfortunately, we cannot trust $templates[0]->theme, since it will always
			// be set to the active theme's slug by _build_block_template_result_from_file(),
			// even if the block template is really coming from the active theme's parent.
			// (The reason for this is that we want it to be associated with the active theme
			// -- not its parent -- once we edit it and store it to the DB as a wp_template CPT.)
			// Instead, we use _get_block_template_file() to locate the block template file.
			$template_file = _get_block_template_file( 'wp_template', $fallback_template_slug );
			if ( $template_file && get_template() === $template_file['theme'] ) {
				// The block template is part of the parent theme, so we
				// have to give precedence to the child theme's PHP template.
				array_shift( $templates );
			}
		}
	}

	return count( $templates ) ? $templates[0] : null;
}

/**
 * Displays title tag with content, regardless of whether theme has title-tag support.
 *
 * @access private
 * @since 5.8.0
 *
 * @see _wp_render_title_tag()
 */
function _block_template_render_title_tag() {
	echo '<title>' . wp_get_document_title() . '</title>' . "\n";
}

/**
 * Returns the markup for the current template.
 *
 * @access private
 * @since 5.8.0
 *
 * @global string   $_wp_current_template_id
 * @global string   $_wp_current_template_content
 * @global WP_Embed $wp_embed                     WordPress Embed object.
 * @global WP_Query $wp_query                     WordPress Query object.
 *
 * @return string Block template markup.
 */
function get_the_block_template_html() {
	global $_wp_current_template_id, $_wp_current_template_content, $wp_embed, $wp_query;

	if ( ! $_wp_current_template_content ) {
		if ( is_user_logged_in() ) {
			return '<h1>' . esc_html__( 'No matching template found' ) . '</h1>';
		}
		return '';
	}

	$content = $wp_embed->run_shortcode( $_wp_current_template_content );
	$content = $wp_embed->autoembed( $content );
	$content = shortcode_unautop( $content );
	$content = do_shortcode( $content );

	/*
	 * Most block themes omit the `core/query` and `core/post-template` blocks in their singular content templates.
	 * While this technically still works since singular content templates are always for only one post, it results in
	 * the main query loop never being entered which causes bugs in core and the plugin ecosystem.
	 *
	 * The workaround below ensures that the loop is started even for those singular templates. The while loop will by
	 * definition only go through a single iteration, i.e. `do_blocks()` is only called once. Additional safeguard
	 * checks are included to ensure the main query loop has not been tampered with and really only encompasses a
	 * single post.
	 *
	 * Even if the block template contained a `core/query` and `core/post-template` block referencing the main query
	 * loop, it would not cause errors since it would use a cloned instance and go through the same loop of a single
	 * post, within the actual main query loop.
	 *
	 * This special logic should be skipped if the current template does not come from the current theme, in which case
	 * it has been injected by a plugin by hijacking the block template loader mechanism. In that case, entirely custom
	 * logic may be applied which is unpredictable and therefore safer to omit this special handling on.
	 */
	if (
		$_wp_current_template_id &&
		str_starts_with( $_wp_current_template_id, get_stylesheet() . '//' ) &&
		is_singular() &&
		1 === $wp_query->post_count &&
		have_posts()
	) {
		while ( have_posts() ) {
			the_post();
			$content = do_blocks( $content );
		}
	} else {
		$content = do_blocks( $content );
	}

	$content = wptexturize( $content );
	$content = convert_smilies( $content );
	$content = wp_filter_content_tags( $content, 'template' );
	$content = str_replace( ']]>', ']]&gt;', $content );

	// Wrap block template in .wp-site-blocks to allow for specific descendant styles
	// (e.g. `.wp-site-blocks > *`).
	return '<div class="wp-site-blocks">' . $content . '</div>';
}

/**
 * Renders a 'viewport' meta tag.
 *
 * This is hooked into {@see 'wp_head'} to decouple its output from the default template canvas.
 *
 * @access private
 * @since 5.8.0
 */
function _block_template_viewport_meta_tag() {
	echo '<meta name="viewport" content="width=device-width, initial-scale=1" />' . "\n";
}

/**
 * Strips .php or .html suffix from template file names.
 *
 * @access private
 * @since 5.8.0
 *
 * @param string $template_file Template file name.
 * @return string Template file name without extension.
 */
function _strip_template_file_suffix( $template_file ) {
	return preg_replace( '/\.(php|html)$/', '', $template_file );
}

/**
 * Removes post details from block context when rendering a block template.
 *
 * @access private
 * @since 5.8.0
 *
 * @param array $context Default context.
 *
 * @return array Filtered context.
 */
function _block_template_render_without_post_block_context( $context ) {
	/*
	 * When loading a template directly and not through a page that resolves it,
	 * the top-level post ID and type context get set to that of the template.
	 * Templates are just the structure of a site, and they should not be available
	 * as post context because blocks like Post Content would recurse infinitely.
	 */
	if ( isset( $context['postType'] ) && 'wp_template' === $context['postType'] ) {
		unset( $context['postId'] );
		unset( $context['postType'] );
	}

	return $context;
}

/**
 * Sets the current WP_Query to return auto-draft posts.
 *
 * The auto-draft status indicates a new post, so allow the the WP_Query instance to
 * return an auto-draft post for template resolution when editing a new post.
 *
 * @access private
 * @since 5.9.0
 *
 * @param WP_Query $wp_query Current WP_Query instance, passed by reference.
 */
function _resolve_template_for_new_post( $wp_query ) {
	if ( ! $wp_query->is_main_query() ) {
		return;
	}

	remove_filter( 'pre_get_posts', '_resolve_template_for_new_post' );

	// Pages.
	$page_id = isset( $wp_query->query['page_id'] ) ? $wp_query->query['page_id'] : null;

	// Posts, including custom post types.
	$p = isset( $wp_query->query['p'] ) ? $wp_query->query['p'] : null;

	$post_id = $page_id ? $page_id : $p;
	$post    = get_post( $post_id );

	if (
		$post &&
		'auto-draft' === $post->post_status &&
		current_user_can( 'edit_post', $post->ID )
	) {
		$wp_query->set( 'post_status', 'auto-draft' );
	}
}

/**
 * Register a block template.
 *
 * @since 6.7.0
 *
 * @param string       $template_name  Template name in the form of `plugin_uri//template_name`.
 * @param array|string $args           {
 *     @type string        $title                 Optional. Title of the template as it will be shown in the Site Editor
 *                                                and other UI elements.
 *     @type string        $description           Optional. Description of the template as it will be shown in the Site
 *                                                Editor.
 *     @type string        $content               Optional. Default content of the template that will be used when the
 *                                                template is rendered or edited in the editor.
 *     @type string[]      $post_types            Optional. Array of post types to which the template should be available.
 *     @type string        $plugin                Optional. Slug of the plugin that registers the template.
 * }
 * @return WP_Block_Template|WP_Error The registered template object on success, WP_Error object on failure.
 */
function register_block_template( $template_name, $args = array() ) {
	return WP_Block_Templates_Registry::get_instance()->register( $template_name, $args );
}

/**
 * Unregister a block template.
 *
 * @since 6.7.0
 *
 * @param string $template_name Template name in the form of `plugin_uri//template_name`.
 * @return WP_Block_Template|WP_Error The unregistered template object on success, WP_Error object on failure or if the
 *                                    template doesn't exist.
 */
function unregister_block_template( $template_name ) {
	return WP_Block_Templates_Registry::get_instance()->unregister( $template_name );
}
 ./wp-includes/block-bindings.php                                                                    0000644 0001750 0001750 00000016546 15152744171 017344  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Block Bindings API
 *
 * Contains functions for managing block bindings in WordPress.
 *
 * @package WordPress
 * @subpackage Block Bindings
 * @since 6.5.0
 */

/**
 * Registers a new block bindings source.
 *
 * Registering a source consists of defining a **name** for that source and a callback function specifying
 * how to get a value from that source and pass it to a block attribute.
 *
 * Once a source is registered, any block that supports the Block Bindings API can use a value
 * from that source by setting its `metadata.bindings` attribute to a value that refers to the source.
 *
 * Note that `register_block_bindings_source()` should be called from a handler attached to the `init` hook.
 *
 *
 * ## Example
 *
 * ### Registering a source
 *
 * First, you need to define a function that will be used to get the value from the source.
 *
 *     function my_plugin_get_custom_source_value( array $source_args, $block_instance, string $attribute_name ) {
 *       // Your custom logic to get the value from the source.
 *       // For example, you can use the `$source_args` to look up a value in a custom table or get it from an external API.
 *       $value = $source_args['key'];
 *
 *       return "The value passed to the block is: $value"
 *     }
 *
 * The `$source_args` will contain the arguments passed to the source in the block's
 * `metadata.bindings` attribute. See the example in the "Usage in a block" section below.
 *
 *     function my_plugin_register_block_bindings_sources() {
 *       register_block_bindings_source( 'my-plugin/my-custom-source', array(
 *         'label'              => __( 'My Custom Source', 'my-plugin' ),
 *         'get_value_callback' => 'my_plugin_get_custom_source_value',
 *       ) );
 *     }
 *     add_action( 'init', 'my_plugin_register_block_bindings_sources' );
 *
 * ### Usage in a block
 *
 * In a block's `metadata.bindings` attribute, you can specify the source and
 * its arguments. Such a block will use the source to override the block
 * attribute's value. For example:
 *
 *     <!-- wp:paragraph {
 *       "metadata": {
 *         "bindings": {
 *           "content": {
 *             "source": "my-plugin/my-custom-source",
 *             "args": {
 *               "key": "you can pass any custom arguments here"
 *             }
 *           }
 *         }
 *       }
 *     } -->
 *     <p>Fallback text that gets replaced.</p>
 *     <!-- /wp:paragraph -->
 *
 * @since 6.5.0
 *
 * @param string $source_name       The name of the source. It must be a string containing a namespace prefix, i.e.
 *                                  `my-plugin/my-custom-source`. It must only contain lowercase alphanumeric
 *                                  characters, the forward slash `/` and dashes.
 * @param array  $source_properties {
 *     The array of arguments that are used to register a source.
 *
 *     @type string   $label              The label of the source.
 *     @type callable $get_value_callback A callback executed when the source is processed during block rendering.
 *                                        The callback should have the following signature:
 *
 *                                        `function( $source_args, $block_instance, $attribute_name ): mixed`
 *                                            - @param array    $source_args    Array containing source arguments
 *                                                                              used to look up the override value,
 *                                                                              i.e. {"key": "foo"}.
 *                                            - @param WP_Block $block_instance The block instance.
 *                                            - @param string   $attribute_name The name of an attribute.
 *                                        The callback has a mixed return type; it may return a string to override
 *                                        the block's original value, null, false to remove an attribute, etc.
 *     @type string[] $uses_context       Optional. Array of values to add to block `uses_context` needed by the source.
 * }
 * @return WP_Block_Bindings_Source|false Source when the registration was successful, or `false` on failure.
 */
function register_block_bindings_source( string $source_name, array $source_properties ) {
	return WP_Block_Bindings_Registry::get_instance()->register( $source_name, $source_properties );
}

/**
 * Unregisters a block bindings source.
 *
 * @since 6.5.0
 *
 * @param string $source_name Block bindings source name including namespace.
 * @return WP_Block_Bindings_Source|false The unregistered block bindings source on success and `false` otherwise.
 */
function unregister_block_bindings_source( string $source_name ) {
	return WP_Block_Bindings_Registry::get_instance()->unregister( $source_name );
}

/**
 * Retrieves the list of all registered block bindings sources.
 *
 * @since 6.5.0
 *
 * @return WP_Block_Bindings_Source[] The array of registered block bindings sources.
 */
function get_all_registered_block_bindings_sources() {
	return WP_Block_Bindings_Registry::get_instance()->get_all_registered();
}

/**
 * Retrieves a registered block bindings source.
 *
 * @since 6.5.0
 *
 * @param string $source_name The name of the source.
 * @return WP_Block_Bindings_Source|null The registered block bindings source, or `null` if it is not registered.
 */
function get_block_bindings_source( string $source_name ) {
	return WP_Block_Bindings_Registry::get_instance()->get_registered( $source_name );
}

/**
 * Retrieves the list of block attributes supported by block bindings.
 *
 * @since 6.9.0
 *
 * @param string $block_type The block type whose supported attributes are being retrieved.
 * @return array The list of block attributes that are supported by block bindings.
 */
function get_block_bindings_supported_attributes( $block_type ) {
	$block_bindings_supported_attributes = array(
		'core/paragraph'          => array( 'content' ),
		'core/heading'            => array( 'content' ),
		'core/image'              => array( 'id', 'url', 'title', 'alt', 'caption' ),
		'core/button'             => array( 'url', 'text', 'linkTarget', 'rel' ),
		'core/post-date'          => array( 'datetime' ),
		'core/navigation-link'    => array( 'url' ),
		'core/navigation-submenu' => array( 'url' ),
	);

	$supported_block_attributes =
		isset( $block_type, $block_bindings_supported_attributes[ $block_type ] ) ?
			$block_bindings_supported_attributes[ $block_type ] :
			array();

	/**
	 * Filters the supported block attributes for block bindings.
	 *
	 * @since 6.9.0
	 *
	 * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
	 * @param string   $block_type                 The block type whose attributes are being filtered.
	 */
	$supported_block_attributes = apply_filters(
		'block_bindings_supported_attributes',
		$supported_block_attributes,
		$block_type
	);

	/**
	 * Filters the supported block attributes for block bindings.
	 *
	 * The dynamic portion of the hook name, `$block_type`, refers to the block type
	 * whose attributes are being filtered.
	 *
	 * @since 6.9.0
	 *
	 * @param string[] $supported_block_attributes The block's attributes that are supported by block bindings.
	 */
	$supported_block_attributes = apply_filters(
		"block_bindings_supported_attributes_{$block_type}",
		$supported_block_attributes
	);

	return $supported_block_attributes;
}
                                                                                                                                                          ./wp-includes/admin-bar.php                                                                         0000644 0001750 0001750 00000110146 15152744171 016300  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Toolbar API: Top-level Toolbar functionality
 *
 * @package WordPress
 * @subpackage Toolbar
 * @since 3.1.0
 */

/**
 * Instantiates the admin bar object and set it up as a global for access elsewhere.
 *
 * UNHOOKING THIS FUNCTION WILL NOT PROPERLY REMOVE THE ADMIN BAR.
 * For that, use show_admin_bar(false) or the {@see 'show_admin_bar'} filter.
 *
 * @since 3.1.0
 * @access private
 *
 * @global WP_Admin_Bar $wp_admin_bar
 *
 * @return bool Whether the admin bar was successfully initialized.
 */
function _wp_admin_bar_init() {
	global $wp_admin_bar;

	if ( ! is_admin_bar_showing() ) {
		return false;
	}

	/* Load the admin bar class code ready for instantiation */
	require_once ABSPATH . WPINC . '/class-wp-admin-bar.php';

	/* Instantiate the admin bar */

	/**
	 * Filters the admin bar class to instantiate.
	 *
	 * @since 3.1.0
	 *
	 * @param string $wp_admin_bar_class Admin bar class to use. Default 'WP_Admin_Bar'.
	 */
	$admin_bar_class = apply_filters( 'wp_admin_bar_class', 'WP_Admin_Bar' );
	if ( class_exists( $admin_bar_class ) ) {
		$wp_admin_bar = new $admin_bar_class();
	} else {
		return false;
	}

	$wp_admin_bar->initialize();
	$wp_admin_bar->add_menus();

	return true;
}

/**
 * Renders the admin bar to the page based on the $wp_admin_bar->menu member var.
 *
 * This is called very early on the {@see 'wp_body_open'} action so that it will render
 * before anything else being added to the page body.
 *
 * For backward compatibility with themes not using the 'wp_body_open' action,
 * the function is also called late on {@see 'wp_footer'}.
 *
 * It includes the {@see 'admin_bar_menu'} action which should be used to hook in and
 * add new menus to the admin bar. This also gives you access to the `$post` global,
 * among others.
 *
 * @since 3.1.0
 * @since 5.4.0 Called on 'wp_body_open' action first, with 'wp_footer' as a fallback.
 *
 * @global WP_Admin_Bar $wp_admin_bar
 */
function wp_admin_bar_render() {
	global $wp_admin_bar;
	static $rendered = false;

	if ( $rendered ) {
		return;
	}

	if ( ! is_admin_bar_showing() || ! is_object( $wp_admin_bar ) ) {
		return;
	}

	/**
	 * Loads all necessary admin bar items.
	 *
	 * This hook can add, remove, or manipulate admin bar items. The priority
	 * determines the placement for new items, and changes to existing items
	 * would require a high priority. To remove or manipulate existing nodes
	 * without a specific priority, use `wp_before_admin_bar_render`.
	 *
	 * @since 3.1.0
	 *
	 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance, passed by reference.
	 */
	do_action_ref_array( 'admin_bar_menu', array( &$wp_admin_bar ) );

	/**
	 * Fires before the admin bar is rendered.
	 *
	 * @since 3.1.0
	 */
	do_action( 'wp_before_admin_bar_render' );

	$wp_admin_bar->render();

	/**
	 * Fires after the admin bar is rendered.
	 *
	 * @since 3.1.0
	 */
	do_action( 'wp_after_admin_bar_render' );

	$rendered = true;
}

/**
 * Adds the WordPress logo menu.
 *
 * @since 3.3.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_wp_menu( $wp_admin_bar ) {
	if ( current_user_can( 'read' ) ) {
		$about_url      = self_admin_url( 'about.php' );
		$contribute_url = self_admin_url( 'contribute.php' );
	} elseif ( is_multisite() ) {
		$about_url      = get_dashboard_url( get_current_user_id(), 'about.php' );
		$contribute_url = get_dashboard_url( get_current_user_id(), 'contribute.php' );
	} else {
		$about_url      = false;
		$contribute_url = false;
	}

	$wp_logo_menu_args = array(
		'id'    => 'wp-logo',
		'title' => '<span class="ab-icon" aria-hidden="true"></span><span class="screen-reader-text">' .
				/* translators: Hidden accessibility text. */
				__( 'About WordPress' ) .
			'</span>',
		'href'  => $about_url,
		'meta'  => array(
			'menu_title' => __( 'About WordPress' ),
		),
	);

	// Set tabindex="0" to make sub menus accessible when no URL is available.
	if ( ! $about_url ) {
		$wp_logo_menu_args['meta'] = array(
			'tabindex' => 0,
		);
	}

	$wp_admin_bar->add_node( $wp_logo_menu_args );

	if ( $about_url ) {
		// Add "About WordPress" link.
		$wp_admin_bar->add_node(
			array(
				'parent' => 'wp-logo',
				'id'     => 'about',
				'title'  => __( 'About WordPress' ),
				'href'   => $about_url,
			)
		);
	}

	if ( $contribute_url ) {
		// Add contribute link.
		$wp_admin_bar->add_node(
			array(
				'parent' => 'wp-logo',
				'id'     => 'contribute',
				'title'  => __( 'Get Involved' ),
				'href'   => $contribute_url,
			)
		);
	}

	// Add WordPress.org link.
	$wp_admin_bar->add_node(
		array(
			'parent' => 'wp-logo-external',
			'id'     => 'wporg',
			'title'  => __( 'WordPress.org' ),
			'href'   => __( 'https://wordpress.org/' ),
		)
	);

	// Add documentation link.
	$wp_admin_bar->add_node(
		array(
			'parent' => 'wp-logo-external',
			'id'     => 'documentation',
			'title'  => __( 'Documentation' ),
			'href'   => __( 'https://wordpress.org/documentation/' ),
		)
	);

	// Add learn link.
	$wp_admin_bar->add_node(
		array(
			'parent' => 'wp-logo-external',
			'id'     => 'learn',
			'title'  => __( 'Learn WordPress' ),
			'href'   => __( 'https://learn.wordpress.org/' ),
		)
	);

	// Add forums link.
	$wp_admin_bar->add_node(
		array(
			'parent' => 'wp-logo-external',
			'id'     => 'support-forums',
			'title'  => __( 'Support' ),
			'href'   => __( 'https://wordpress.org/support/forums/' ),
		)
	);

	// Add feedback link.
	$wp_admin_bar->add_node(
		array(
			'parent' => 'wp-logo-external',
			'id'     => 'feedback',
			'title'  => __( 'Feedback' ),
			'href'   => __( 'https://wordpress.org/support/forum/requests-and-feedback' ),
		)
	);
}

/**
 * Adds the sidebar toggle button.
 *
 * @since 3.8.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_sidebar_toggle( $wp_admin_bar ) {
	if ( is_admin() ) {
		$wp_admin_bar->add_node(
			array(
				'id'    => 'menu-toggle',
				'title' => '<span class="ab-icon" aria-hidden="true"></span><span class="screen-reader-text">' .
						/* translators: Hidden accessibility text. */
						__( 'Menu' ) .
					'</span>',
				'href'  => '#',
			)
		);
	}
}

/**
 * Adds the "My Account" item.
 *
 * @since 3.3.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_my_account_item( $wp_admin_bar ) {
	$user_id = get_current_user_id();
	if ( ! $user_id ) {
		return;
	}

	if ( current_user_can( 'read' ) ) {
		$profile_url = get_edit_profile_url( $user_id );
	} elseif ( is_multisite() ) {
		$profile_url = get_dashboard_url( $user_id, 'profile.php' );
	} else {
		$profile_url = false;
	}

	/* translators: %s: Current user's display name. */
	$howdy = sprintf( __( 'Howdy, %s' ), '<span class="display-name">' . wp_get_current_user()->display_name . '</span>' );

	$avatar = get_avatar( $user_id, 26 );
	$wp_admin_bar->add_node(
		array(
			'id'     => 'my-account',
			'parent' => 'top-secondary',
			'title'  => $howdy . $avatar,
			'href'   => $profile_url,
			'meta'   => array(
				'class'      => empty( $avatar ) ? '' : 'with-avatar',
				'menu_title' => wp_strip_all_tags( $howdy ),
				'tabindex'   => ( false !== $profile_url ) ? '' : 0,
			),
		)
	);
}

/**
 * Adds the "My Account" submenu items.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_my_account_menu( $wp_admin_bar ) {
	$user_id      = get_current_user_id();
	$current_user = wp_get_current_user();

	if ( ! $user_id ) {
		return;
	}

	if ( current_user_can( 'read' ) ) {
		$profile_url = get_edit_profile_url( $user_id );
	} elseif ( is_multisite() ) {
		$profile_url = get_dashboard_url( $user_id, 'profile.php' );
	} else {
		$profile_url = false;
	}

	$wp_admin_bar->add_group(
		array(
			'parent' => 'my-account',
			'id'     => 'user-actions',
		)
	);

	$user_info  = get_avatar( $user_id, 64 );
	$user_info .= "<span class='display-name'>{$current_user->display_name}</span>";

	if ( $current_user->display_name !== $current_user->user_login ) {
		$user_info .= "<span class='username'>{$current_user->user_login}</span>";
	}

	if ( false !== $profile_url ) {
		$user_info .= "<span class='display-name edit-profile'>" . __( 'Edit Profile' ) . '</span>';
	}

	$wp_admin_bar->add_node(
		array(
			'parent' => 'user-actions',
			'id'     => 'user-info',
			'title'  => $user_info,
			'href'   => $profile_url,
		)
	);

	$wp_admin_bar->add_node(
		array(
			'parent' => 'user-actions',
			'id'     => 'logout',
			'title'  => __( 'Log Out' ),
			'href'   => wp_logout_url(),
		)
	);
}

/**
 * Adds the "Site Name" menu.
 *
 * @since 3.3.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_site_menu( $wp_admin_bar ) {
	// Don't show for logged out users.
	if ( ! is_user_logged_in() ) {
		return;
	}

	// Show only when the user is a member of this site, or they're a super admin.
	if ( ! is_user_member_of_blog() && ! current_user_can( 'manage_network' ) ) {
		return;
	}

	$blogname = get_bloginfo( 'name' );

	if ( ! $blogname ) {
		$blogname = preg_replace( '#^(https?://)?(www\.)?#', '', get_home_url() );
	}

	if ( is_network_admin() ) {
		/* translators: %s: Site title. */
		$blogname = sprintf( __( 'Network Admin: %s' ), esc_html( get_network()->site_name ) );
	} elseif ( is_user_admin() ) {
		/* translators: %s: Site title. */
		$blogname = sprintf( __( 'User Dashboard: %s' ), esc_html( get_network()->site_name ) );
	}

	$title = wp_html_excerpt( $blogname, 40, '&hellip;' );

	$wp_admin_bar->add_node(
		array(
			'id'    => 'site-name',
			'title' => $title,
			'href'  => ( is_admin() || ! current_user_can( 'read' ) ) ? home_url( '/' ) : admin_url(),
			'meta'  => array(
				'menu_title' => $title,
			),
		)
	);

	// Create submenu items.

	if ( is_admin() ) {
		// Add an option to visit the site.
		$wp_admin_bar->add_node(
			array(
				'parent' => 'site-name',
				'id'     => 'view-site',
				'title'  => __( 'Visit Site' ),
				'href'   => home_url( '/' ),
			)
		);

		if ( is_blog_admin() && is_multisite() && current_user_can( 'manage_sites' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'site-name',
					'id'     => 'edit-site',
					'title'  => __( 'Manage Site' ),
					'href'   => network_admin_url( 'site-info.php?id=' . get_current_blog_id() ),
				)
			);
		}
	} elseif ( current_user_can( 'read' ) ) {
		// We're on the front end, link to the Dashboard.
		$wp_admin_bar->add_node(
			array(
				'parent' => 'site-name',
				'id'     => 'dashboard',
				'title'  => __( 'Dashboard' ),
				'href'   => admin_url(),
			)
		);

		// Add the appearance submenu items.
		wp_admin_bar_appearance_menu( $wp_admin_bar );

		// Add a Plugins link.
		if ( current_user_can( 'activate_plugins' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'site-name',
					'id'     => 'plugins',
					'title'  => __( 'Plugins' ),
					'href'   => admin_url( 'plugins.php' ),
				)
			);
		}
	}
}

/**
 * Adds the "Edit Site" link to the Toolbar.
 *
 * @since 5.9.0
 * @since 6.3.0 Added `$_wp_current_template_id` global for editing of current template directly from the admin bar.
 * @since 6.6.0 Added the `canvas` query arg to the Site Editor link.
 *
 * @global string $_wp_current_template_id
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_edit_site_menu( $wp_admin_bar ) {
	global $_wp_current_template_id;

	// Don't show if a block theme is not activated.
	if ( ! wp_is_block_theme() ) {
		return;
	}

	// Don't show for users who can't edit theme options or when in the admin.
	if ( ! current_user_can( 'edit_theme_options' ) || is_admin() ) {
		return;
	}

	$wp_admin_bar->add_node(
		array(
			'id'    => 'site-editor',
			'title' => __( 'Edit Site' ),
			'href'  => add_query_arg(
				array(
					'postType' => 'wp_template',
					'postId'   => $_wp_current_template_id,
					'canvas'   => 'edit',
				),
				admin_url( 'site-editor.php' )
			),
		)
	);
}

/**
 * Adds the "Customize" link to the Toolbar.
 *
 * @since 4.3.0
 *
 * @global WP_Customize_Manager $wp_customize
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_customize_menu( $wp_admin_bar ) {
	global $wp_customize;

	// Don't show if a block theme is activated and no plugins use the customizer.
	if ( wp_is_block_theme() && ! has_action( 'customize_register' ) ) {
		return;
	}

	// Don't show for users who can't access the customizer or when in the admin.
	if ( ! current_user_can( 'customize' ) || is_admin() ) {
		return;
	}

	// Don't show if the user cannot edit a given customize_changeset post currently being previewed.
	if ( is_customize_preview() && $wp_customize->changeset_post_id()
		&& ! current_user_can( get_post_type_object( 'customize_changeset' )->cap->edit_post, $wp_customize->changeset_post_id() )
	) {
		return;
	}

	$current_url = ( is_ssl() ? 'https://' : 'http://' ) . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
	if ( is_customize_preview() && $wp_customize->changeset_uuid() ) {
		$current_url = remove_query_arg( 'customize_changeset_uuid', $current_url );
	}

	$customize_url = add_query_arg( 'url', urlencode( $current_url ), wp_customize_url() );
	if ( is_customize_preview() ) {
		$customize_url = add_query_arg( array( 'changeset_uuid' => $wp_customize->changeset_uuid() ), $customize_url );
	}

	$wp_admin_bar->add_node(
		array(
			'id'    => 'customize',
			'title' => __( 'Customize' ),
			'href'  => $customize_url,
			'meta'  => array(
				'class' => 'hide-if-no-customize',
			),
		)
	);
	add_action( 'wp_before_admin_bar_render', 'wp_customize_support_script' );
}

/**
 * Adds the "My Sites/[Site Name]" menu and all submenus.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_my_sites_menu( $wp_admin_bar ) {
	// Don't show for logged out users or single site mode.
	if ( ! is_user_logged_in() || ! is_multisite() ) {
		return;
	}

	// Show only when the user has at least one site, or they're a super admin.
	if ( count( $wp_admin_bar->user->blogs ) < 1 && ! current_user_can( 'manage_network' ) ) {
		return;
	}

	if ( $wp_admin_bar->user->active_blog ) {
		$my_sites_url = get_admin_url( $wp_admin_bar->user->active_blog->blog_id, 'my-sites.php' );
	} else {
		$my_sites_url = admin_url( 'my-sites.php' );
	}

	$wp_admin_bar->add_node(
		array(
			'id'    => 'my-sites',
			'title' => __( 'My Sites' ),
			'href'  => $my_sites_url,
		)
	);

	if ( current_user_can( 'manage_network' ) ) {
		$wp_admin_bar->add_group(
			array(
				'parent' => 'my-sites',
				'id'     => 'my-sites-super-admin',
			)
		);

		$wp_admin_bar->add_node(
			array(
				'parent' => 'my-sites-super-admin',
				'id'     => 'network-admin',
				'title'  => __( 'Network Admin' ),
				'href'   => network_admin_url(),
			)
		);

		$wp_admin_bar->add_node(
			array(
				'parent' => 'network-admin',
				'id'     => 'network-admin-d',
				'title'  => __( 'Dashboard' ),
				'href'   => network_admin_url(),
			)
		);

		if ( current_user_can( 'manage_sites' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'network-admin',
					'id'     => 'network-admin-s',
					'title'  => __( 'Sites' ),
					'href'   => network_admin_url( 'sites.php' ),
				)
			);
		}

		if ( current_user_can( 'manage_network_users' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'network-admin',
					'id'     => 'network-admin-u',
					'title'  => __( 'Users' ),
					'href'   => network_admin_url( 'users.php' ),
				)
			);
		}

		if ( current_user_can( 'manage_network_themes' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'network-admin',
					'id'     => 'network-admin-t',
					'title'  => __( 'Themes' ),
					'href'   => network_admin_url( 'themes.php' ),
				)
			);
		}

		if ( current_user_can( 'manage_network_plugins' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'network-admin',
					'id'     => 'network-admin-p',
					'title'  => __( 'Plugins' ),
					'href'   => network_admin_url( 'plugins.php' ),
				)
			);
		}

		if ( current_user_can( 'manage_network_options' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'network-admin',
					'id'     => 'network-admin-o',
					'title'  => __( 'Settings' ),
					'href'   => network_admin_url( 'settings.php' ),
				)
			);
		}
	}

	// Add site links.
	$wp_admin_bar->add_group(
		array(
			'parent' => 'my-sites',
			'id'     => 'my-sites-list',
			'meta'   => array(
				'class' => current_user_can( 'manage_network' ) ? 'ab-sub-secondary' : '',
			),
		)
	);

	/**
	 * Filters whether to show the site icons in toolbar.
	 *
	 * Returning false to this hook is the recommended way to hide site icons in the toolbar.
	 * A truthy return may have negative performance impact on large multisites.
	 *
	 * @since 6.0.0
	 *
	 * @param bool $show_site_icons Whether site icons should be shown in the toolbar. Default true.
	 */
	$show_site_icons = apply_filters( 'wp_admin_bar_show_site_icons', true );

	foreach ( (array) $wp_admin_bar->user->blogs as $blog ) {
		switch_to_blog( $blog->userblog_id );

		if ( true === $show_site_icons && has_site_icon() ) {
			$blavatar = sprintf(
				'<img class="blavatar" src="%s" srcset="%s 2x" alt="" width="16" height="16"%s />',
				esc_url( get_site_icon_url( 16 ) ),
				esc_url( get_site_icon_url( 32 ) ),
				( wp_lazy_loading_enabled( 'img', 'site_icon_in_toolbar' ) ? ' loading="lazy"' : '' )
			);
		} else {
			$blavatar = '<div class="blavatar"></div>';
		}

		$blogname = $blog->blogname;

		if ( ! $blogname ) {
			$blogname = preg_replace( '#^(https?://)?(www\.)?#', '', get_home_url() );
		}

		$menu_id = 'blog-' . $blog->userblog_id;

		if ( current_user_can( 'read' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'my-sites-list',
					'id'     => $menu_id,
					'title'  => $blavatar . $blogname,
					'href'   => admin_url(),
				)
			);

			$wp_admin_bar->add_node(
				array(
					'parent' => $menu_id,
					'id'     => $menu_id . '-d',
					'title'  => __( 'Dashboard' ),
					'href'   => admin_url(),
				)
			);
		} else {
			$wp_admin_bar->add_node(
				array(
					'parent' => 'my-sites-list',
					'id'     => $menu_id,
					'title'  => $blavatar . $blogname,
					'href'   => home_url(),
				)
			);
		}

		if ( current_user_can( get_post_type_object( 'post' )->cap->create_posts ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => $menu_id,
					'id'     => $menu_id . '-n',
					'title'  => get_post_type_object( 'post' )->labels->new_item,
					'href'   => admin_url( 'post-new.php' ),
				)
			);
		}

		if ( current_user_can( 'edit_posts' ) ) {
			$wp_admin_bar->add_node(
				array(
					'parent' => $menu_id,
					'id'     => $menu_id . '-c',
					'title'  => __( 'Manage Comments' ),
					'href'   => admin_url( 'edit-comments.php' ),
				)
			);
		}

		$wp_admin_bar->add_node(
			array(
				'parent' => $menu_id,
				'id'     => $menu_id . '-v',
				'title'  => __( 'Visit Site' ),
				'href'   => home_url( '/' ),
			)
		);

		restore_current_blog();
	}
}

/**
 * Provides a shortlink.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_shortlink_menu( $wp_admin_bar ) {
	$short = wp_get_shortlink( 0, 'query' );
	$id    = 'get-shortlink';

	if ( empty( $short ) ) {
		return;
	}

	$html = '<input class="shortlink-input" type="text" readonly="readonly" value="' . esc_attr( $short ) . '" aria-label="' . __( 'Shortlink' ) . '" />';

	$wp_admin_bar->add_node(
		array(
			'id'    => $id,
			'title' => __( 'Shortlink' ),
			'href'  => $short,
			'meta'  => array( 'html' => $html ),
		)
	);
}

/**
 * Provides an edit link for posts and terms.
 *
 * @since 3.1.0
 * @since 5.5.0 Added a "View Post" link on Comments screen for a single post.
 *
 * @global WP_Term  $tag
 * @global WP_Query $wp_the_query WordPress Query object.
 * @global int      $user_id      The ID of the user being edited. Not to be confused with the
 *                                global $user_ID, which contains the ID of the current user.
 * @global int      $post_id      The ID of the post when editing comments for a single post.
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_edit_menu( $wp_admin_bar ) {
	global $tag, $wp_the_query, $user_id, $post_id;

	if ( is_admin() ) {
		$current_screen   = get_current_screen();
		$post             = get_post();
		$post_type_object = null;

		if ( 'post' === $current_screen->base ) {
			$post_type_object = get_post_type_object( $post->post_type );
		} elseif ( 'edit' === $current_screen->base ) {
			$post_type_object = get_post_type_object( $current_screen->post_type );
		} elseif ( 'edit-comments' === $current_screen->base && $post_id ) {
			$post = get_post( $post_id );
			if ( $post ) {
				$post_type_object = get_post_type_object( $post->post_type );
			}
		}

		if ( ( 'post' === $current_screen->base || 'edit-comments' === $current_screen->base )
			&& 'add' !== $current_screen->action
			&& ( $post_type_object )
			&& current_user_can( 'read_post', $post->ID )
			&& ( $post_type_object->public )
			&& ( $post_type_object->show_in_admin_bar ) ) {
			if ( 'draft' === $post->post_status ) {
				$preview_link = get_preview_post_link( $post );
				$wp_admin_bar->add_node(
					array(
						'id'    => 'preview',
						'title' => $post_type_object->labels->view_item,
						'href'  => esc_url( $preview_link ),
						'meta'  => array( 'target' => 'wp-preview-' . $post->ID ),
					)
				);
			} else {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'view',
						'title' => $post_type_object->labels->view_item,
						'href'  => get_permalink( $post->ID ),
					)
				);
			}
		} elseif ( 'edit' === $current_screen->base
			&& ( $post_type_object )
			&& ( $post_type_object->public )
			&& ( $post_type_object->show_in_admin_bar )
			&& ( get_post_type_archive_link( $post_type_object->name ) )
			&& ! ( 'post' === $post_type_object->name && 'posts' === get_option( 'show_on_front' ) ) ) {
			$wp_admin_bar->add_node(
				array(
					'id'    => 'archive',
					'title' => $post_type_object->labels->view_items,
					'href'  => get_post_type_archive_link( $current_screen->post_type ),
				)
			);
		} elseif ( 'term' === $current_screen->base && isset( $tag ) && is_object( $tag ) && ! is_wp_error( $tag ) ) {
			$tax = get_taxonomy( $tag->taxonomy );
			if ( is_term_publicly_viewable( $tag ) ) {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'view',
						'title' => $tax->labels->view_item,
						'href'  => get_term_link( $tag ),
					)
				);
			}
		} elseif ( 'user-edit' === $current_screen->base && isset( $user_id ) ) {
			$user_object = get_userdata( $user_id );
			$view_link   = get_author_posts_url( $user_object->ID );
			if ( $user_object->exists() && $view_link ) {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'view',
						'title' => __( 'View User' ),
						'href'  => $view_link,
					)
				);
			}
		}
	} else {
		$current_object = $wp_the_query->get_queried_object();

		if ( empty( $current_object ) ) {
			return;
		}

		if ( ! empty( $current_object->post_type ) ) {
			$post_type_object = get_post_type_object( $current_object->post_type );
			$edit_post_link   = get_edit_post_link( $current_object->ID );
			if ( $post_type_object
				&& $edit_post_link
				&& current_user_can( 'edit_post', $current_object->ID )
				&& $post_type_object->show_in_admin_bar ) {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'edit',
						'title' => $post_type_object->labels->edit_item,
						'href'  => $edit_post_link,
					)
				);
			}
		} elseif ( ! empty( $current_object->taxonomy ) ) {
			$tax            = get_taxonomy( $current_object->taxonomy );
			$edit_term_link = get_edit_term_link( $current_object->term_id, $current_object->taxonomy );
			if ( $tax && $edit_term_link && current_user_can( 'edit_term', $current_object->term_id ) ) {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'edit',
						'title' => $tax->labels->edit_item,
						'href'  => $edit_term_link,
					)
				);
			}
		} elseif ( $current_object instanceof WP_User && current_user_can( 'edit_user', $current_object->ID ) ) {
			$edit_user_link = get_edit_user_link( $current_object->ID );
			if ( $edit_user_link ) {
				$wp_admin_bar->add_node(
					array(
						'id'    => 'edit',
						'title' => __( 'Edit User' ),
						'href'  => $edit_user_link,
					)
				);
			}
		}
	}
}

/**
 * Adds "Add New" menu.
 *
 * @since 3.1.0
 * @since 6.5.0 Added a New Site link for network installations.
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_new_content_menu( $wp_admin_bar ) {
	$actions = array();

	$cpts = (array) get_post_types( array( 'show_in_admin_bar' => true ), 'objects' );

	if ( isset( $cpts['post'] ) && current_user_can( $cpts['post']->cap->create_posts ) ) {
		$actions['post-new.php'] = array( $cpts['post']->labels->name_admin_bar, 'new-post' );
	}

	if ( isset( $cpts['attachment'] ) && current_user_can( 'upload_files' ) ) {
		$actions['media-new.php'] = array( $cpts['attachment']->labels->name_admin_bar, 'new-media' );
	}

	if ( current_user_can( 'manage_links' ) ) {
		$actions['link-add.php'] = array( _x( 'Link', 'add new from admin bar' ), 'new-link' );
	}

	if ( isset( $cpts['page'] ) && current_user_can( $cpts['page']->cap->create_posts ) ) {
		$actions['post-new.php?post_type=page'] = array( $cpts['page']->labels->name_admin_bar, 'new-page' );
	}

	unset( $cpts['post'], $cpts['page'], $cpts['attachment'] );

	// Add any additional custom post types.
	foreach ( $cpts as $cpt ) {
		if ( ! current_user_can( $cpt->cap->create_posts ) ) {
			continue;
		}

		$key             = 'post-new.php?post_type=' . $cpt->name;
		$actions[ $key ] = array( $cpt->labels->name_admin_bar, 'new-' . $cpt->name );
	}
	// Avoid clash with parent node and a 'content' post type.
	if ( isset( $actions['post-new.php?post_type=content'] ) ) {
		$actions['post-new.php?post_type=content'][1] = 'add-new-content';
	}

	if ( current_user_can( 'create_users' ) || ( is_multisite() && current_user_can( 'promote_users' ) ) ) {
		$actions['user-new.php'] = array( _x( 'User', 'add new from admin bar' ), 'new-user' );
	}

	if ( ! $actions ) {
		return;
	}

	$title = '<span class="ab-icon" aria-hidden="true"></span><span class="ab-label">' . _x( 'New', 'admin bar menu group label' ) . '</span>';

	$wp_admin_bar->add_node(
		array(
			'id'    => 'new-content',
			'title' => $title,
			'href'  => admin_url( current( array_keys( $actions ) ) ),
			'meta'  => array(
				'menu_title' => _x( 'New', 'admin bar menu group label' ),
			),
		)
	);

	foreach ( $actions as $link => $action ) {
		list( $title, $id ) = $action;

		$wp_admin_bar->add_node(
			array(
				'parent' => 'new-content',
				'id'     => $id,
				'title'  => $title,
				'href'   => admin_url( $link ),
			)
		);
	}

	if ( is_multisite() && current_user_can( 'create_sites' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'new-content',
				'id'     => 'add-new-site',
				'title'  => _x( 'Site', 'add new from admin bar' ),
				'href'   => network_admin_url( 'site-new.php' ),
			)
		);
	}
}

/**
 * Adds edit comments link with awaiting moderation count bubble.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_comments_menu( $wp_admin_bar ) {
	if ( ! current_user_can( 'edit_posts' ) ) {
		return;
	}

	$awaiting_mod  = wp_count_comments();
	$awaiting_mod  = $awaiting_mod->moderated;
	$awaiting_text = sprintf(
		/* translators: Hidden accessibility text. %s: Number of comments. */
		_n( '%s Comment in moderation', '%s Comments in moderation', $awaiting_mod ),
		number_format_i18n( $awaiting_mod )
	);

	$icon   = '<span class="ab-icon" aria-hidden="true"></span>';
	$title  = '<span class="ab-label awaiting-mod pending-count count-' . $awaiting_mod . '" aria-hidden="true">' . number_format_i18n( $awaiting_mod ) . '</span>';
	$title .= '<span class="screen-reader-text comments-in-moderation-text">' . $awaiting_text . '</span>';

	$wp_admin_bar->add_node(
		array(
			'id'    => 'comments',
			'title' => $icon . $title,
			'href'  => admin_url( 'edit-comments.php' ),
		)
	);
}

/**
 * Adds appearance submenu items to the "Site Name" menu.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_appearance_menu( $wp_admin_bar ) {
	$wp_admin_bar->add_group(
		array(
			'parent' => 'site-name',
			'id'     => 'appearance',
		)
	);

	if ( current_user_can( 'switch_themes' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'appearance',
				'id'     => 'themes',
				'title'  => __( 'Themes' ),
				'href'   => admin_url( 'themes.php' ),
			)
		);
	}

	if ( ! current_user_can( 'edit_theme_options' ) ) {
		return;
	}

	if ( current_theme_supports( 'widgets' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'appearance',
				'id'     => 'widgets',
				'title'  => __( 'Widgets' ),
				'href'   => admin_url( 'widgets.php' ),
			)
		);
	}

	if ( current_theme_supports( 'menus' ) || current_theme_supports( 'widgets' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'appearance',
				'id'     => 'menus',
				'title'  => __( 'Menus' ),
				'href'   => admin_url( 'nav-menus.php' ),
			)
		);
	}

	if ( current_theme_supports( 'custom-background' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'appearance',
				'id'     => 'background',
				'title'  => _x( 'Background', 'custom background' ),
				'href'   => admin_url( 'themes.php?page=custom-background' ),
				'meta'   => array(
					'class' => 'hide-if-customize',
				),
			)
		);
	}

	if ( current_theme_supports( 'custom-header' ) ) {
		$wp_admin_bar->add_node(
			array(
				'parent' => 'appearance',
				'id'     => 'header',
				'title'  => _x( 'Header', 'custom image header' ),
				'href'   => admin_url( 'themes.php?page=custom-header' ),
				'meta'   => array(
					'class' => 'hide-if-customize',
				),
			)
		);
	}
}

/**
 * Provides an update link if theme/plugin/core updates are available.
 *
 * @since 3.1.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_updates_menu( $wp_admin_bar ) {

	$update_data = wp_get_update_data();

	if ( ! $update_data['counts']['total'] ) {
		return;
	}

	$updates_text = sprintf(
		/* translators: Hidden accessibility text. %s: Total number of updates available. */
		_n( '%s update available', '%s updates available', $update_data['counts']['total'] ),
		number_format_i18n( $update_data['counts']['total'] )
	);

	$icon   = '<span class="ab-icon" aria-hidden="true"></span>';
	$title  = '<span class="ab-label" aria-hidden="true">' . number_format_i18n( $update_data['counts']['total'] ) . '</span>';
	$title .= '<span class="screen-reader-text updates-available-text">' . $updates_text . '</span>';

	$wp_admin_bar->add_node(
		array(
			'id'    => 'updates',
			'title' => $icon . $title,
			'href'  => network_admin_url( 'update-core.php' ),
		)
	);
}

/**
 * Adds search form.
 *
 * @since 3.3.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_search_menu( $wp_admin_bar ) {
	if ( is_admin() ) {
		return;
	}

	$form  = '<form action="' . esc_url( home_url( '/' ) ) . '" method="get" id="adminbarsearch">';
	$form .= '<input class="adminbar-input" name="s" id="adminbar-search" type="text" value="" maxlength="150" />';
	$form .= '<label for="adminbar-search" class="screen-reader-text">' .
			/* translators: Hidden accessibility text. */
			__( 'Search' ) .
		'</label>';
	$form .= '<input type="submit" class="adminbar-button" value="' . __( 'Search' ) . '" />';
	$form .= '</form>';

	$wp_admin_bar->add_node(
		array(
			'parent' => 'top-secondary',
			'id'     => 'search',
			'title'  => $form,
			'meta'   => array(
				'class'    => 'admin-bar-search',
				'tabindex' => -1,
			),
		)
	);
}

/**
 * Adds a link to exit recovery mode when Recovery Mode is active.
 *
 * @since 5.2.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_recovery_mode_menu( $wp_admin_bar ) {
	if ( ! wp_is_recovery_mode() ) {
		return;
	}

	$url = wp_login_url();
	$url = add_query_arg( 'action', WP_Recovery_Mode::EXIT_ACTION, $url );
	$url = wp_nonce_url( $url, WP_Recovery_Mode::EXIT_ACTION );

	$wp_admin_bar->add_node(
		array(
			'parent' => 'top-secondary',
			'id'     => 'recovery-mode',
			'title'  => __( 'Exit Recovery Mode' ),
			'href'   => $url,
		)
	);
}

/**
 * Adds secondary menus.
 *
 * @since 3.3.0
 *
 * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance.
 */
function wp_admin_bar_add_secondary_groups( $wp_admin_bar ) {
	$wp_admin_bar->add_group(
		array(
			'id'   => 'top-secondary',
			'meta' => array(
				'class' => 'ab-top-secondary',
			),
		)
	);

	$wp_admin_bar->add_group(
		array(
			'parent' => 'wp-logo',
			'id'     => 'wp-logo-external',
			'meta'   => array(
				'class' => 'ab-sub-secondary',
			),
		)
	);
}

/**
 * Enqueues inline style to hide the admin bar when printing.
 *
 * @since 6.4.0
 */
function wp_enqueue_admin_bar_header_styles() {
	// Back-compat for plugins that disable functionality by unhooking this action.
	$action = is_admin() ? 'admin_head' : 'wp_head';
	if ( ! has_action( $action, 'wp_admin_bar_header' ) ) {
		return;
	}
	remove_action( $action, 'wp_admin_bar_header' );

	wp_add_inline_style( 'admin-bar', '@media print { #wpadminbar { display:none; } }' );
}

/**
 * Enqueues inline bump styles to make room for the admin bar.
 *
 * @since 6.4.0
 */
function wp_enqueue_admin_bar_bump_styles() {
	if ( current_theme_supports( 'admin-bar' ) ) {
		$admin_bar_args  = get_theme_support( 'admin-bar' );
		$header_callback = $admin_bar_args[0]['callback'];
	}

	if ( empty( $header_callback ) ) {
		$header_callback = '_admin_bar_bump_cb';
	}

	if ( '_admin_bar_bump_cb' !== $header_callback ) {
		return;
	}

	// Back-compat for plugins that disable functionality by unhooking this action.
	if ( ! has_action( 'wp_head', $header_callback ) ) {
		return;
	}
	remove_action( 'wp_head', $header_callback );

	$css = '
		@media screen { html { margin-top: 32px !important; } }
		@media screen and ( max-width: 782px ) { html { margin-top: 46px !important; } }
	';
	wp_add_inline_style( 'admin-bar', $css );
}

/**
 * Sets the display status of the admin bar.
 *
 * This can be called immediately upon plugin load. It does not need to be called
 * from a function hooked to the {@see 'init'} action.
 *
 * @since 3.1.0
 *
 * @global bool $show_admin_bar
 *
 * @param bool $show Whether to allow the admin bar to show.
 */
function show_admin_bar( $show ) {
	global $show_admin_bar;
	$show_admin_bar = (bool) $show;
}

/**
 * Determines whether the admin bar should be showing.
 *
 * For more information on this and similar theme functions, check out
 * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
 * Conditional Tags} article in the Theme Developer Handbook.
 *
 * @since 3.1.0
 *
 * @global bool   $show_admin_bar
 * @global string $pagenow        The filename of the current screen.
 *
 * @return bool Whether the admin bar should be showing.
 */
function is_admin_bar_showing() {
	global $show_admin_bar, $pagenow;

	// For all these types of requests, we never want an admin bar.
	if ( defined( 'XMLRPC_REQUEST' ) || defined( 'DOING_AJAX' ) || defined( 'IFRAME_REQUEST' ) || wp_is_json_request() ) {
		return false;
	}

	if ( is_embed() ) {
		return false;
	}

	// Integrated into the admin.
	if ( is_admin() ) {
		return true;
	}

	if ( ! isset( $show_admin_bar ) ) {
		if ( ! is_user_logged_in() || 'wp-login.php' === $pagenow ) {
			$show_admin_bar = false;
		} else {
			$show_admin_bar = _get_admin_bar_pref();
		}
	}

	/**
	 * Filters whether to show the admin bar.
	 *
	 * Returning false to this hook is the recommended way to hide the admin bar.
	 * The user's display preference is used for logged in users.
	 *
	 * @since 3.1.0
	 *
	 * @param bool $show_admin_bar Whether the admin bar should be shown. Default false.
	 */
	$show_admin_bar = apply_filters( 'show_admin_bar', $show_admin_bar );

	return $show_admin_bar;
}

/**
 * Retrieves the admin bar display preference of a user.
 *
 * @since 3.1.0
 * @access private
 *
 * @param string $context Context of this preference check. Defaults to 'front'. The 'admin'
 *                        preference is no longer used.
 * @param int    $user    Optional. ID of the user to check, defaults to 0 for current user.
 * @return bool Whether the admin bar should be showing for this user.
 */
function _get_admin_bar_pref( $context = 'front', $user = 0 ) {
	$pref = get_user_option( "show_admin_bar_{$context}", $user );
	if ( false === $pref ) {
		return true;
	}

	return 'true' === $pref;
}
                                                                                                                                                                                                                                                                                                                                                                                                                          ./wp-includes/block-patterns.php                                                                    0000644 0001750 0001750 00000031635 15152744167 017410  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Register the block patterns and block patterns categories
 *
 * @package WordPress
 * @since 5.5.0
 */

add_theme_support( 'core-block-patterns' );

/**
 * Registers the core block patterns and categories.
 *
 * @since 5.5.0
 * @since 6.3.0 Added source to core block patterns.
 * @access private
 */
function _register_core_block_patterns_and_categories() {
	$should_register_core_patterns = get_theme_support( 'core-block-patterns' );

	if ( $should_register_core_patterns ) {
		$core_block_patterns = array(
			'query-standard-posts',
			'query-medium-posts',
			'query-small-posts',
			'query-grid-posts',
			'query-large-title-posts',
			'query-offset-posts',
		);

		foreach ( $core_block_patterns as $core_block_pattern ) {
			$pattern           = require __DIR__ . '/block-patterns/' . $core_block_pattern . '.php';
			$pattern['source'] = 'core';
			register_block_pattern( 'core/' . $core_block_pattern, $pattern );
		}
	}

	register_block_pattern_category(
		'banner',
		array(
			'label'       => _x( 'Banners', 'Block pattern category' ),
			'description' => __( 'Bold sections designed to showcase key content.' ),
		)
	);
	register_block_pattern_category(
		'buttons',
		array(
			'label'       => _x( 'Buttons', 'Block pattern category' ),
			'description' => __( 'Patterns that contain buttons and call to actions.' ),
		)
	);
	register_block_pattern_category(
		'columns',
		array(
			'label'       => _x( 'Columns', 'Block pattern category' ),
			'description' => __( 'Multi-column patterns with more complex layouts.' ),
		)
	);
	register_block_pattern_category(
		'text',
		array(
			'label'       => _x( 'Text', 'Block pattern category' ),
			'description' => __( 'Patterns containing mostly text.' ),
		)
	);
	register_block_pattern_category(
		'query',
		array(
			'label'       => _x( 'Posts', 'Block pattern category' ),
			'description' => __( 'Display your latest posts in lists, grids or other layouts.' ),
		)
	);
	register_block_pattern_category(
		'featured',
		array(
			'label'       => _x( 'Featured', 'Block pattern category' ),
			'description' => __( 'A set of high quality curated patterns.' ),
		)
	);
	register_block_pattern_category(
		'call-to-action',
		array(
			'label'       => _x( 'Call to action', 'Block pattern category' ),
			'description' => __( 'Sections whose purpose is to trigger a specific action.' ),
		)
	);
	register_block_pattern_category(
		'team',
		array(
			'label'       => _x( 'Team', 'Block pattern category' ),
			'description' => __( 'A variety of designs to display your team members.' ),
		)
	);
	register_block_pattern_category(
		'testimonials',
		array(
			'label'       => _x( 'Testimonials', 'Block pattern category' ),
			'description' => __( 'Share reviews and feedback about your brand/business.' ),
		)
	);
	register_block_pattern_category(
		'services',
		array(
			'label'       => _x( 'Services', 'Block pattern category' ),
			'description' => __( 'Briefly describe what your business does and how you can help.' ),
		)
	);
	register_block_pattern_category(
		'contact',
		array(
			'label'       => _x( 'Contact', 'Block pattern category' ),
			'description' => __( 'Display your contact information.' ),
		)
	);
	register_block_pattern_category(
		'about',
		array(
			'label'       => _x( 'About', 'Block pattern category' ),
			'description' => __( 'Introduce yourself.' ),
		)
	);
	register_block_pattern_category(
		'portfolio',
		array(
			'label'       => _x( 'Portfolio', 'Block pattern category' ),
			'description' => __( 'Showcase your latest work.' ),
		)
	);
	register_block_pattern_category(
		'gallery',
		array(
			'label'       => _x( 'Gallery', 'Block pattern category' ),
			'description' => __( 'Different layouts for displaying images.' ),
		)
	);
	register_block_pattern_category(
		'media',
		array(
			'label'       => _x( 'Media', 'Block pattern category' ),
			'description' => __( 'Different layouts containing video or audio.' ),
		)
	);
	register_block_pattern_category(
		'videos',
		array(
			'label'       => _x( 'Videos', 'Block pattern category' ),
			'description' => __( 'Different layouts containing videos.' ),
		)
	);
	register_block_pattern_category(
		'audio',
		array(
			'label'       => _x( 'Audio', 'Block pattern category' ),
			'description' => __( 'Different layouts containing audio.' ),
		)
	);
	register_block_pattern_category(
		'posts',
		array(
			'label'       => _x( 'Posts', 'Block pattern category' ),
			'description' => __( 'Display your latest posts in lists, grids or other layouts.' ),
		)
	);
	register_block_pattern_category(
		'footer',
		array(
			'label'       => _x( 'Footers', 'Block pattern category' ),
			'description' => __( 'A variety of footer designs displaying information and site navigation.' ),
		)
	);
	register_block_pattern_category(
		'header',
		array(
			'label'       => _x( 'Headers', 'Block pattern category' ),
			'description' => __( 'A variety of header designs displaying your site title and navigation.' ),
		)
	);
}

/**
 * Normalize the pattern properties to camelCase.
 *
 * The API's format is snake_case, `register_block_pattern()` expects camelCase.
 *
 * @since 6.2.0
 * @access private
 *
 * @param array $pattern Pattern as returned from the Pattern Directory API.
 * @return array Normalized pattern.
 */
function wp_normalize_remote_block_pattern( $pattern ) {
	if ( isset( $pattern['block_types'] ) ) {
		$pattern['blockTypes'] = $pattern['block_types'];
		unset( $pattern['block_types'] );
	}

	if ( isset( $pattern['viewport_width'] ) ) {
		$pattern['viewportWidth'] = $pattern['viewport_width'];
		unset( $pattern['viewport_width'] );
	}

	return (array) $pattern;
}

/**
 * Register Core's official patterns from wordpress.org/patterns.
 *
 * @since 5.8.0
 * @since 5.9.0 The $current_screen argument was removed.
 * @since 6.2.0 Normalize the pattern from the API (snake_case) to the
 *              format expected by `register_block_pattern` (camelCase).
 * @since 6.3.0 Add 'pattern-directory/core' to the pattern's 'source'.
 *
 * @param WP_Screen $deprecated Unused. Formerly the screen that the current request was triggered from.
 */
function _load_remote_block_patterns( $deprecated = null ) {
	if ( ! empty( $deprecated ) ) {
		_deprecated_argument( __FUNCTION__, '5.9.0' );
		$current_screen = $deprecated;
		if ( ! $current_screen->is_block_editor ) {
			return;
		}
	}

	$supports_core_patterns = get_theme_support( 'core-block-patterns' );

	/**
	 * Filter to disable remote block patterns.
	 *
	 * @since 5.8.0
	 *
	 * @param bool $should_load_remote
	 */
	$should_load_remote = apply_filters( 'should_load_remote_block_patterns', true );

	if ( $supports_core_patterns && $should_load_remote ) {
		$request         = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' );
		$core_keyword_id = 11; // 11 is the ID for "core".
		$request->set_param( 'keyword', $core_keyword_id );
		$response = rest_do_request( $request );
		if ( $response->is_error() ) {
			return;
		}
		$patterns = $response->get_data();

		foreach ( $patterns as $pattern ) {
			$pattern['source']  = 'pattern-directory/core';
			$normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
			$pattern_name       = 'core/' . sanitize_title( $normalized_pattern['title'] );
			register_block_pattern( $pattern_name, $normalized_pattern );
		}
	}
}

/**
 * Register `Featured` (category) patterns from wordpress.org/patterns.
 *
 * @since 5.9.0
 * @since 6.2.0 Normalized the pattern from the API (snake_case) to the
 *              format expected by `register_block_pattern()` (camelCase).
 * @since 6.3.0 Add 'pattern-directory/featured' to the pattern's 'source'.
 */
function _load_remote_featured_patterns() {
	$supports_core_patterns = get_theme_support( 'core-block-patterns' );

	/** This filter is documented in wp-includes/block-patterns.php */
	$should_load_remote = apply_filters( 'should_load_remote_block_patterns', true );

	if ( ! $should_load_remote || ! $supports_core_patterns ) {
		return;
	}

	$request         = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' );
	$featured_cat_id = 26; // This is the `Featured` category id from pattern directory.
	$request->set_param( 'category', $featured_cat_id );
	$response = rest_do_request( $request );
	if ( $response->is_error() ) {
		return;
	}
	$patterns = $response->get_data();
	$registry = WP_Block_Patterns_Registry::get_instance();
	foreach ( $patterns as $pattern ) {
		$pattern['source']  = 'pattern-directory/featured';
		$normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
		$pattern_name       = sanitize_title( $normalized_pattern['title'] );
		// Some patterns might be already registered as core patterns with the `core` prefix.
		$is_registered = $registry->is_registered( $pattern_name ) || $registry->is_registered( "core/$pattern_name" );
		if ( ! $is_registered ) {
			register_block_pattern( $pattern_name, $normalized_pattern );
		}
	}
}

/**
 * Registers patterns from Pattern Directory provided by a theme's
 * `theme.json` file.
 *
 * @since 6.0.0
 * @since 6.2.0 Normalized the pattern from the API (snake_case) to the
 *              format expected by `register_block_pattern()` (camelCase).
 * @since 6.3.0 Add 'pattern-directory/theme' to the pattern's 'source'.
 * @access private
 */
function _register_remote_theme_patterns() {
	/** This filter is documented in wp-includes/block-patterns.php */
	if ( ! apply_filters( 'should_load_remote_block_patterns', true ) ) {
		return;
	}

	if ( ! wp_theme_has_theme_json() ) {
		return;
	}

	$pattern_settings = wp_get_theme_directory_pattern_slugs();
	if ( empty( $pattern_settings ) ) {
		return;
	}

	$request         = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' );
	$request['slug'] = $pattern_settings;
	$response        = rest_do_request( $request );
	if ( $response->is_error() ) {
		return;
	}
	$patterns          = $response->get_data();
	$patterns_registry = WP_Block_Patterns_Registry::get_instance();
	foreach ( $patterns as $pattern ) {
		$pattern['source']  = 'pattern-directory/theme';
		$normalized_pattern = wp_normalize_remote_block_pattern( $pattern );
		$pattern_name       = sanitize_title( $normalized_pattern['title'] );
		// Some patterns might be already registered as core patterns with the `core` prefix.
		$is_registered = $patterns_registry->is_registered( $pattern_name ) || $patterns_registry->is_registered( "core/$pattern_name" );
		if ( ! $is_registered ) {
			register_block_pattern( $pattern_name, $normalized_pattern );
		}
	}
}

/**
 * Register any patterns that the active theme may provide under its
 * `./patterns/` directory.
 *
 * @since 6.0.0
 * @since 6.1.0 The `postTypes` property was added.
 * @since 6.2.0 The `templateTypes` property was added.
 * @since 6.4.0 Uses the `WP_Theme::get_block_patterns` method.
 * @access private
 */
function _register_theme_block_patterns() {

	/*
	 * During the bootstrap process, a check for active and valid themes is run.
	 * If no themes are returned, the theme's functions.php file will not be loaded,
	 * which can lead to errors if patterns expect some variables or constants to
	 * already be set at this point, so bail early if that is the case.
	 */
	if ( empty( wp_get_active_and_valid_themes() ) ) {
		return;
	}

	/*
	 * Register patterns for the active theme. If the theme is a child theme,
	 * let it override any patterns from the parent theme that shares the same slug.
	 */
	$themes   = array();
	$theme    = wp_get_theme();
	$themes[] = $theme;
	if ( $theme->parent() ) {
		$themes[] = $theme->parent();
	}
	$registry = WP_Block_Patterns_Registry::get_instance();

	foreach ( $themes as $theme ) {
		$patterns    = $theme->get_block_patterns();
		$dirpath     = $theme->get_stylesheet_directory() . '/patterns/';
		$text_domain = $theme->get( 'TextDomain' );

		foreach ( $patterns as $file => $pattern_data ) {
			if ( $registry->is_registered( $pattern_data['slug'] ) ) {
				continue;
			}

			$file_path = $dirpath . $file;

			if ( ! file_exists( $file_path ) ) {
				_doing_it_wrong(
					__FUNCTION__,
					sprintf(
						/* translators: %s: file name. */
						__( 'Could not register file "%s" as a block pattern as the file does not exist.' ),
						$file
					),
					'6.4.0'
				);
				$theme->delete_pattern_cache();
				continue;
			}

			$pattern_data['filePath'] = $file_path;

			// Translate the pattern metadata.
			// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
			$pattern_data['title'] = translate_with_gettext_context( $pattern_data['title'], 'Pattern title', $text_domain );
			if ( ! empty( $pattern_data['description'] ) ) {
				// phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralDomain,WordPress.WP.I18n.LowLevelTranslationFunction
				$pattern_data['description'] = translate_with_gettext_context( $pattern_data['description'], 'Pattern description', $text_domain );
			}

			register_block_pattern( $pattern_data['slug'], $pattern_data );
		}
	}
}
add_action( 'init', '_register_theme_block_patterns' );
                                                                                                   ./wp-includes/ms-functions.php                                                                      0000644 0001750 0001750 00000263302 15152744172 017077  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * Multisite WordPress API
 *
 * @package WordPress
 * @subpackage Multisite
 * @since 3.0.0
 */

/**
 * Gets the network's site and user counts.
 *
 * @since MU (3.0.0)
 *
 * @return int[] {
 *     Site and user count for the network.
 *
 *     @type int $blogs Number of sites on the network.
 *     @type int $users Number of users on the network.
 * }
 */
function get_sitestats() {
	$stats = array(
		'blogs' => get_blog_count(),
		'users' => get_user_count(),
	);

	return $stats;
}

/**
 * Gets one of a user's active blogs.
 *
 * Returns the user's primary blog, if they have one and
 * it is active. If it's inactive, function returns another
 * active blog of the user. If none are found, the user
 * is added as a Subscriber to the Dashboard Blog and that blog
 * is returned.
 *
 * @since MU (3.0.0)
 *
 * @param int $user_id The unique ID of the user
 * @return WP_Site|void The blog object
 */
function get_active_blog_for_user( $user_id ) {
	$blogs = get_blogs_of_user( $user_id );
	if ( empty( $blogs ) ) {
		return;
	}

	if ( ! is_multisite() ) {
		return $blogs[ get_current_blog_id() ];
	}

	$primary_blog = get_user_meta( $user_id, 'primary_blog', true );
	$first_blog   = current( $blogs );
	if ( false !== $primary_blog ) {
		if ( ! isset( $blogs[ $primary_blog ] ) ) {
			update_user_meta( $user_id, 'primary_blog', $first_blog->userblog_id );
			$primary = get_site( $first_blog->userblog_id );
		} else {
			$primary = get_site( $primary_blog );
		}
	} else {
		// TODO: Review this call to add_user_to_blog too - to get here the user must have a role on this blog?
		$result = add_user_to_blog( $first_blog->userblog_id, $user_id, 'subscriber' );

		if ( ! is_wp_error( $result ) ) {
			update_user_meta( $user_id, 'primary_blog', $first_blog->userblog_id );
			$primary = $first_blog;
		}
	}

	if ( ( ! is_object( $primary ) )
		|| ( '1' === $primary->archived || '1' === $primary->spam || '1' === $primary->deleted )
	) {
		$blogs = get_blogs_of_user( $user_id, true ); // If a user's primary blog is shut down, check their other blogs.
		$ret   = false;

		if ( is_array( $blogs ) && count( $blogs ) > 0 ) {
			$current_network_id = get_current_network_id();

			foreach ( (array) $blogs as $blog_id => $blog ) {
				if ( $blog->site_id !== $current_network_id ) {
					continue;
				}

				$details = get_site( $blog_id );
				if ( is_object( $details )
					&& '0' === $details->archived && '0' === $details->spam && '0' === $details->deleted
				) {
					$ret = $details;
					if ( (int) get_user_meta( $user_id, 'primary_blog', true ) !== $blog_id ) {
						update_user_meta( $user_id, 'primary_blog', $blog_id );
					}
					if ( ! get_user_meta( $user_id, 'source_domain', true ) ) {
						update_user_meta( $user_id, 'source_domain', $details->domain );
					}
					break;
				}
			}
		} else {
			return;
		}

		return $ret;
	} else {
		return $primary;
	}
}

/**
 * Gets the number of active sites on the installation.
 *
 * The count is cached and updated twice daily. This is not a live count.
 *
 * @since MU (3.0.0)
 * @since 3.7.0 The `$network_id` parameter has been deprecated.
 * @since 4.8.0 The `$network_id` parameter is now being used.
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 * @return int Number of active sites on the network.
 */
function get_blog_count( $network_id = null ) {
	return get_network_option( $network_id, 'blog_count' );
}

/**
 * Gets a blog post from any site on the network.
 *
 * This function is similar to get_post(), except that it can retrieve a post
 * from any site on the network, not just the current site.
 *
 * @since MU (3.0.0)
 *
 * @param int $blog_id ID of the blog.
 * @param int $post_id ID of the post being looked for.
 * @return WP_Post|null WP_Post object on success, null on failure
 */
function get_blog_post( $blog_id, $post_id ) {
	switch_to_blog( $blog_id );
	$post = get_post( $post_id );
	restore_current_blog();

	return $post;
}

/**
 * Adds a user to a blog, along with specifying the user's role.
 *
 * Use the {@see 'add_user_to_blog'} action to fire an event when users are added to a blog.
 *
 * @since MU (3.0.0)
 *
 * @param int    $blog_id ID of the blog the user is being added to.
 * @param int    $user_id ID of the user being added.
 * @param string $role    User role.
 * @return true|WP_Error True on success or a WP_Error object if the user doesn't exist
 *                       or could not be added.
 */
function add_user_to_blog( $blog_id, $user_id, $role ) {
	switch_to_blog( $blog_id );

	$user = get_userdata( $user_id );

	if ( ! $user ) {
		restore_current_blog();
		return new WP_Error( 'user_does_not_exist', __( 'The requested user does not exist.' ) );
	}

	/**
	 * Filters whether a user should be added to a site.
	 *
	 * @since 4.9.0
	 *
	 * @param true|WP_Error $retval  True if the user should be added to the site, error
	 *                               object otherwise.
	 * @param int           $user_id User ID.
	 * @param string        $role    User role.
	 * @param int           $blog_id Site ID.
	 */
	$can_add_user = apply_filters( 'can_add_user_to_blog', true, $user_id, $role, $blog_id );

	if ( true !== $can_add_user ) {
		restore_current_blog();

		if ( is_wp_error( $can_add_user ) ) {
			return $can_add_user;
		}

		return new WP_Error( 'user_cannot_be_added', __( 'User cannot be added to this site.' ) );
	}

	if ( ! get_user_meta( $user_id, 'primary_blog', true ) ) {
		update_user_meta( $user_id, 'primary_blog', $blog_id );
		$site = get_site( $blog_id );
		update_user_meta( $user_id, 'source_domain', $site->domain );
	}

	$user->set_role( $role );

	/**
	 * Fires immediately after a user is added to a site.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param int    $user_id User ID.
	 * @param string $role    User role.
	 * @param int    $blog_id Blog ID.
	 */
	do_action( 'add_user_to_blog', $user_id, $role, $blog_id );

	clean_user_cache( $user_id );
	wp_cache_delete( $blog_id . '_user_count', 'blog-details' );

	restore_current_blog();

	return true;
}

/**
 * Removes a user from a blog.
 *
 * Use the {@see 'remove_user_from_blog'} action to fire an event when
 * users are removed from a blog.
 *
 * Accepts an optional `$reassign` parameter, if you want to
 * reassign the user's blog posts to another user upon removal.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $user_id  ID of the user being removed.
 * @param int $blog_id  Optional. ID of the blog the user is being removed from. Default 0.
 * @param int $reassign Optional. ID of the user to whom to reassign posts. Default 0.
 * @return true|WP_Error True on success or a WP_Error object if the user doesn't exist.
 */
function remove_user_from_blog( $user_id, $blog_id = 0, $reassign = 0 ) {
	global $wpdb;

	$user_id = (int) $user_id;
	$blog_id = (int) $blog_id;

	switch_to_blog( $blog_id );

	/**
	 * Fires before a user is removed from a site.
	 *
	 * @since MU (3.0.0)
	 * @since 5.4.0 Added the `$reassign` parameter.
	 *
	 * @param int $user_id  ID of the user being removed.
	 * @param int $blog_id  ID of the blog the user is being removed from.
	 * @param int $reassign ID of the user to whom to reassign posts.
	 */
	do_action( 'remove_user_from_blog', $user_id, $blog_id, $reassign );

	/*
	 * If being removed from the primary blog, set a new primary
	 * if the user is assigned to multiple blogs.
	 */
	$primary_blog = (int) get_user_meta( $user_id, 'primary_blog', true );
	if ( $primary_blog === $blog_id ) {
		$new_id     = '';
		$new_domain = '';
		$blogs      = get_blogs_of_user( $user_id );
		foreach ( (array) $blogs as $blog ) {
			if ( $blog->userblog_id === $blog_id ) {
				continue;
			}
			$new_id     = $blog->userblog_id;
			$new_domain = $blog->domain;
			break;
		}

		update_user_meta( $user_id, 'primary_blog', $new_id );
		update_user_meta( $user_id, 'source_domain', $new_domain );
	}

	$user = get_userdata( $user_id );
	if ( ! $user ) {
		restore_current_blog();
		return new WP_Error( 'user_does_not_exist', __( 'That user does not exist.' ) );
	}

	$user->remove_all_caps();

	$blogs = get_blogs_of_user( $user_id );
	if ( count( $blogs ) === 0 ) {
		update_user_meta( $user_id, 'primary_blog', '' );
		update_user_meta( $user_id, 'source_domain', '' );
	}

	if ( $reassign ) {
		$reassign = (int) $reassign;
		$post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_author = %d", $user_id ) );
		$link_ids = $wpdb->get_col( $wpdb->prepare( "SELECT link_id FROM $wpdb->links WHERE link_owner = %d", $user_id ) );

		if ( ! empty( $post_ids ) ) {
			$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET post_author = %d WHERE post_author = %d", $reassign, $user_id ) );
			array_walk( $post_ids, 'clean_post_cache' );
		}

		if ( ! empty( $link_ids ) ) {
			$wpdb->query( $wpdb->prepare( "UPDATE $wpdb->links SET link_owner = %d WHERE link_owner = %d", $reassign, $user_id ) );
			array_walk( $link_ids, 'clean_bookmark_cache' );
		}
	}

	clean_user_cache( $user_id );
	restore_current_blog();

	return true;
}

/**
 * Gets the permalink for a post on another blog.
 *
 * @since MU (3.0.0) 1.0
 *
 * @param int $blog_id ID of the source blog.
 * @param int $post_id ID of the desired post.
 * @return string The post's permalink.
 */
function get_blog_permalink( $blog_id, $post_id ) {
	switch_to_blog( $blog_id );
	$link = get_permalink( $post_id );
	restore_current_blog();

	return $link;
}

/**
 * Gets a blog's numeric ID from its URL.
 *
 * On a subdirectory installation like example.com/blog1/,
 * $domain will be the root 'example.com' and $path the
 * subdirectory '/blog1/'. With subdomains like blog1.example.com,
 * $domain is 'blog1.example.com' and $path is '/'.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $domain Website domain.
 * @param string $path   Optional. Not required for subdomain installations. Default '/'.
 * @return int 0 if no blog found, otherwise the ID of the matching blog.
 */
function get_blog_id_from_url( $domain, $path = '/' ) {
	$domain = strtolower( $domain );
	$path   = strtolower( $path );
	$id     = wp_cache_get( md5( $domain . $path ), 'blog-id-cache' );

	if ( -1 === $id ) { // Blog does not exist.
		return 0;
	} elseif ( $id ) {
		return (int) $id;
	}

	$args   = array(
		'domain'                 => $domain,
		'path'                   => $path,
		'fields'                 => 'ids',
		'number'                 => 1,
		'update_site_meta_cache' => false,
	);
	$result = get_sites( $args );
	$id     = array_shift( $result );

	if ( ! $id ) {
		wp_cache_set( md5( $domain . $path ), -1, 'blog-id-cache' );
		return 0;
	}

	wp_cache_set( md5( $domain . $path ), $id, 'blog-id-cache' );

	return $id;
}

//
// Admin functions.
//

/**
 * Checks an email address against a list of banned domains.
 *
 * This function checks against the Banned Email Domains list
 * at wp-admin/network/settings.php. The check is only run on
 * self-registrations; user creation at wp-admin/network/users.php
 * bypasses this check.
 *
 * @since MU (3.0.0)
 *
 * @param string $user_email The email provided by the user at registration.
 * @return bool True when the email address is banned, false otherwise.
 */
function is_email_address_unsafe( $user_email ) {
	$banned_names = get_site_option( 'banned_email_domains' );
	if ( $banned_names && ! is_array( $banned_names ) ) {
		$banned_names = explode( "\n", $banned_names );
	}

	$is_email_address_unsafe = false;

	if ( $banned_names && is_array( $banned_names ) && false !== strpos( $user_email, '@', 1 ) ) {
		$banned_names     = array_map( 'strtolower', $banned_names );
		$normalized_email = strtolower( $user_email );

		list( $email_local_part, $email_domain ) = explode( '@', $normalized_email );

		foreach ( $banned_names as $banned_domain ) {
			if ( ! $banned_domain ) {
				continue;
			}

			if ( $email_domain === $banned_domain ) {
				$is_email_address_unsafe = true;
				break;
			}

			if ( str_ends_with( $normalized_email, ".$banned_domain" ) ) {
				$is_email_address_unsafe = true;
				break;
			}
		}
	}

	/**
	 * Filters whether an email address is unsafe.
	 *
	 * @since 3.5.0
	 *
	 * @param bool   $is_email_address_unsafe Whether the email address is "unsafe". Default false.
	 * @param string $user_email              User email address.
	 */
	return apply_filters( 'is_email_address_unsafe', $is_email_address_unsafe, $user_email );
}

/**
 * Sanitizes and validates data required for a user sign-up.
 *
 * Verifies the validity and uniqueness of user names and user email addresses,
 * and checks email addresses against allowed and disallowed domains provided by
 * administrators.
 *
 * The {@see 'wpmu_validate_user_signup'} hook provides an easy way to modify the sign-up
 * process. The value $result, which is passed to the hook, contains both the user-provided
 * info and the error messages created by the function. {@see 'wpmu_validate_user_signup'}
 * allows you to process the data in any way you'd like, and unset the relevant errors if
 * necessary.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $user_name  The login name provided by the user.
 * @param string $user_email The email provided by the user.
 * @return array {
 *     The array of user name, email, and the error messages.
 *
 *     @type string   $user_name     Sanitized and unique username.
 *     @type string   $orig_username Original username.
 *     @type string   $user_email    User email address.
 *     @type WP_Error $errors        WP_Error object containing any errors found.
 * }
 */
function wpmu_validate_user_signup( $user_name, $user_email ) {
	global $wpdb;

	$errors = new WP_Error();

	$orig_username = $user_name;
	$user_name     = preg_replace( '/\s+/', '', sanitize_user( $user_name, true ) );

	if ( $user_name !== $orig_username || preg_match( '/[^a-z0-9]/', $user_name ) ) {
		$errors->add( 'user_name', __( 'Usernames can only contain lowercase letters (a-z) and numbers.' ) );
		$user_name = $orig_username;
	}

	$user_email = sanitize_email( $user_email );

	if ( empty( $user_name ) ) {
		$errors->add( 'user_name', __( 'Please enter a username.' ) );
	}

	$illegal_names = get_site_option( 'illegal_names' );

	if ( ! is_array( $illegal_names ) ) {
		$illegal_names = array( 'www', 'web', 'root', 'admin', 'main', 'invite', 'administrator' );
		add_site_option( 'illegal_names', $illegal_names );
	}

	if ( in_array( $user_name, $illegal_names, true ) ) {
		$errors->add( 'user_name', __( 'Sorry, that username is not allowed.' ) );
	}

	/** This filter is documented in wp-includes/user.php */
	$illegal_logins = (array) apply_filters( 'illegal_user_logins', array() );

	if ( in_array( strtolower( $user_name ), array_map( 'strtolower', $illegal_logins ), true ) ) {
		$errors->add( 'user_name', __( 'Sorry, that username is not allowed.' ) );
	}

	if ( ! is_email( $user_email ) ) {
		$errors->add( 'user_email', __( 'Please enter a valid email address.' ) );
	} elseif ( is_email_address_unsafe( $user_email ) ) {
		$errors->add( 'user_email', __( 'You cannot use that email address to signup. There are problems with them blocking some emails from WordPress. Please use another email provider.' ) );
	}

	if ( strlen( $user_name ) < 4 ) {
		$errors->add( 'user_name', __( 'Username must be at least 4 characters.' ) );
	}

	if ( strlen( $user_name ) > 60 ) {
		$errors->add( 'user_name', __( 'Username may not be longer than 60 characters.' ) );
	}

	// All numeric?
	if ( preg_match( '/^[0-9]*$/', $user_name ) ) {
		$errors->add( 'user_name', __( 'Sorry, usernames must have letters too!' ) );
	}

	$limited_email_domains = get_site_option( 'limited_email_domains' );

	if ( is_array( $limited_email_domains ) && ! empty( $limited_email_domains ) ) {
		$limited_email_domains = array_map( 'strtolower', $limited_email_domains );
		$email_domain          = strtolower( substr( $user_email, 1 + strpos( $user_email, '@' ) ) );

		if ( ! in_array( $email_domain, $limited_email_domains, true ) ) {
			$errors->add( 'user_email', __( 'Sorry, that email address is not allowed!' ) );
		}
	}

	// Check if the username has been used already.
	if ( username_exists( $user_name ) ) {
		$errors->add( 'user_name', __( 'Sorry, that username already exists!' ) );
	}

	// Check if the email address has been used already.
	if ( email_exists( $user_email ) ) {
		$errors->add(
			'user_email',
			sprintf(
				/* translators: %s: Link to the login page. */
				__( '<strong>Error:</strong> This email address is already registered. <a href="%s">Log in</a> with this address or choose another one.' ),
				wp_login_url()
			)
		);
	}

	// Has someone already signed up for this username?
	$signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE user_login = %s", $user_name ) );
	if ( $signup instanceof stdClass ) {
		$registered_at = mysql2date( 'U', $signup->registered );
		$now           = time();
		$diff          = $now - $registered_at;
		// If registered more than two days ago, cancel registration and let this signup go through.
		if ( $diff > 2 * DAY_IN_SECONDS ) {
			$wpdb->delete( $wpdb->signups, array( 'user_login' => $user_name ) );
		} else {
			$errors->add( 'user_name', __( 'That username is currently reserved but may be available in a couple of days.' ) );
		}
	}

	$signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE user_email = %s", $user_email ) );
	if ( $signup instanceof stdClass ) {
		$diff = time() - mysql2date( 'U', $signup->registered );
		// If registered more than two days ago, cancel registration and let this signup go through.
		if ( $diff > 2 * DAY_IN_SECONDS ) {
			$wpdb->delete( $wpdb->signups, array( 'user_email' => $user_email ) );
		} else {
			$errors->add( 'user_email', __( 'That email address is pending activation and is not available for new registration. If you made a previous attempt with this email address, please check your inbox for an activation email. If left unconfirmed, it will become available in a couple of days.' ) );
		}
	}

	$result = array(
		'user_name'     => $user_name,
		'orig_username' => $orig_username,
		'user_email'    => $user_email,
		'errors'        => $errors,
	);

	/**
	 * Filters the validated user registration details.
	 *
	 * This does not allow you to override the username or email of the user during
	 * registration. The values are solely used for validation and error handling.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param array $result {
	 *     The array of user name, email, and the error messages.
	 *
	 *     @type string   $user_name     Sanitized and unique username.
	 *     @type string   $orig_username Original username.
	 *     @type string   $user_email    User email address.
	 *     @type WP_Error $errors        WP_Error object containing any errors found.
	 * }
	 */
	return apply_filters( 'wpmu_validate_user_signup', $result );
}

/**
 * Processes new site registrations.
 *
 * Checks the data provided by the user during blog signup. Verifies
 * the validity and uniqueness of blog paths and domains.
 *
 * This function prevents the current user from registering a new site
 * with a blogname equivalent to another user's login name. Passing the
 * $user parameter to the function, where $user is the other user, is
 * effectively an override of this limitation.
 *
 * Filter {@see 'wpmu_validate_blog_signup'} if you want to modify
 * the way that WordPress validates new site signups.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb   $wpdb   WordPress database abstraction object.
 * @global string $domain
 *
 * @param string         $blogname   The site name provided by the user. Must be unique.
 * @param string         $blog_title The site title provided by the user.
 * @param WP_User|string $user       Optional. The user object to check against the new site name.
 *                                   Default empty string.
 * @return array {
 *     Array of domain, path, site name, site title, user and error messages.
 *
 *     @type string         $domain     Domain for the site.
 *     @type string         $path       Path for the site. Used in subdirectory installations.
 *     @type string         $blogname   The unique site name (slug).
 *     @type string         $blog_title Blog title.
 *     @type string|WP_User $user       By default, an empty string. A user object if provided.
 *     @type WP_Error       $errors     WP_Error containing any errors found.
 * }
 */
function wpmu_validate_blog_signup( $blogname, $blog_title, $user = '' ) {
	global $wpdb, $domain;

	$current_network = get_network();
	$base            = $current_network->path;

	$blog_title = strip_tags( $blog_title );

	$errors        = new WP_Error();
	$illegal_names = get_site_option( 'illegal_names' );

	if ( ! is_array( $illegal_names ) ) {
		$illegal_names = array( 'www', 'web', 'root', 'admin', 'main', 'invite', 'administrator' );
		add_site_option( 'illegal_names', $illegal_names );
	}

	/*
	 * On sub dir installations, some names are so illegal, only a filter can
	 * spring them from jail.
	 */
	if ( ! is_subdomain_install() ) {
		$illegal_names = array_merge( $illegal_names, get_subdirectory_reserved_names() );
	}

	if ( empty( $blogname ) ) {
		$errors->add( 'blogname', __( 'Please enter a site name.' ) );
	}

	if ( preg_match( '/[^a-z0-9]+/', $blogname ) ) {
		$errors->add( 'blogname', __( 'Site names can only contain lowercase letters (a-z) and numbers.' ) );
	}

	if ( in_array( $blogname, $illegal_names, true ) ) {
		$errors->add( 'blogname', __( 'That name is not allowed.' ) );
	}

	/**
	 * Filters the minimum site name length required when validating a site signup.
	 *
	 * @since 4.8.0
	 *
	 * @param int $length The minimum site name length. Default 4.
	 */
	$minimum_site_name_length = apply_filters( 'minimum_site_name_length', 4 );

	if ( strlen( $blogname ) < $minimum_site_name_length ) {
		/* translators: %s: Minimum site name length. */
		$errors->add( 'blogname', sprintf( _n( 'Site name must be at least %s character.', 'Site name must be at least %s characters.', $minimum_site_name_length ), number_format_i18n( $minimum_site_name_length ) ) );
	}

	// Do not allow users to create a site that conflicts with a page on the main blog.
	if ( ! is_subdomain_install() && $wpdb->get_var( $wpdb->prepare( 'SELECT post_name FROM ' . $wpdb->get_blog_prefix( $current_network->site_id ) . "posts WHERE post_type = 'page' AND post_name = %s", $blogname ) ) ) {
		$errors->add( 'blogname', __( 'Sorry, you may not use that site name.' ) );
	}

	// All numeric?
	if ( preg_match( '/^[0-9]*$/', $blogname ) ) {
		$errors->add( 'blogname', __( 'Sorry, site names must have letters too!' ) );
	}

	/**
	 * Filters the new site name during registration.
	 *
	 * The name is the site's subdomain or the site's subdirectory
	 * path depending on the network settings.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $blogname Site name.
	 */
	$blogname = apply_filters( 'newblogname', $blogname );

	$blog_title = wp_unslash( $blog_title );

	if ( empty( $blog_title ) ) {
		$errors->add( 'blog_title', __( 'Please enter a site title.' ) );
	}

	// Check if the domain/path has been used already.
	if ( is_subdomain_install() ) {
		$mydomain = $blogname . '.' . preg_replace( '|^www\.|', '', $domain );
		$path     = $base;
	} else {
		$mydomain = $domain;
		$path     = $base . $blogname . '/';
	}
	if ( domain_exists( $mydomain, $path, $current_network->id ) ) {
		$errors->add( 'blogname', __( 'Sorry, that site already exists!' ) );
	}

	/*
	 * Do not allow users to create a site that matches an existing user's login name,
	 * unless it's the user's own username.
	 */
	if ( username_exists( $blogname ) ) {
		if ( ! $user instanceof WP_User || $user->user_login !== $blogname ) {
			$errors->add( 'blogname', __( 'Sorry, that site is reserved!' ) );
		}
	}

	/*
	 * Has someone already signed up for this domain?
	 * TODO: Check email too?
	 */
	$signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE domain = %s AND path = %s", $mydomain, $path ) );
	if ( $signup instanceof stdClass ) {
		$diff = time() - mysql2date( 'U', $signup->registered );
		// If registered more than two days ago, cancel registration and let this signup go through.
		if ( $diff > 2 * DAY_IN_SECONDS ) {
			$wpdb->delete(
				$wpdb->signups,
				array(
					'domain' => $mydomain,
					'path'   => $path,
				)
			);
		} else {
			$errors->add( 'blogname', __( 'That site is currently reserved but may be available in a couple days.' ) );
		}
	}

	$result = array(
		'domain'     => $mydomain,
		'path'       => $path,
		'blogname'   => $blogname,
		'blog_title' => $blog_title,
		'user'       => $user,
		'errors'     => $errors,
	);

	/**
	 * Filters site details and error messages following registration.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param array $result {
	 *     Array of domain, path, site name, site title, user and error messages.
	 *
	 *     @type string         $domain     Domain for the site.
	 *     @type string         $path       Path for the site. Used in subdirectory installations.
	 *     @type string         $blogname   The unique site name (slug).
	 *     @type string         $blog_title Site title.
	 *     @type string|WP_User $user       By default, an empty string. A user object if provided.
	 *     @type WP_Error       $errors     WP_Error containing any errors found.
	 * }
	 */
	return apply_filters( 'wpmu_validate_blog_signup', $result );
}

/**
 * Records site signup information for future activation.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $domain     The requested domain.
 * @param string $path       The requested path.
 * @param string $title      The requested site title.
 * @param string $user       The user's requested login name.
 * @param string $user_email The user's email address.
 * @param array  $meta       Optional. Signup meta data. By default, contains the requested privacy setting and lang_id.
 */
function wpmu_signup_blog( $domain, $path, $title, $user, $user_email, $meta = array() ) {
	global $wpdb;

	$key = substr( md5( time() . wp_rand() . $domain ), 0, 16 );

	/**
	 * Filters the metadata for a site signup.
	 *
	 * The metadata will be serialized prior to storing it in the database.
	 *
	 * @since 4.8.0
	 *
	 * @param array  $meta       Signup meta data. Default empty array.
	 * @param string $domain     The requested domain.
	 * @param string $path       The requested path.
	 * @param string $title      The requested site title.
	 * @param string $user       The user's requested login name.
	 * @param string $user_email The user's email address.
	 * @param string $key        The user's activation key.
	 */
	$meta = apply_filters( 'signup_site_meta', $meta, $domain, $path, $title, $user, $user_email, $key );

	$wpdb->insert(
		$wpdb->signups,
		array(
			'domain'         => $domain,
			'path'           => $path,
			'title'          => $title,
			'user_login'     => $user,
			'user_email'     => $user_email,
			'registered'     => current_time( 'mysql', true ),
			'activation_key' => $key,
			'meta'           => serialize( $meta ),
		)
	);

	/**
	 * Fires after site signup information has been written to the database.
	 *
	 * @since 4.4.0
	 *
	 * @param string $domain     The requested domain.
	 * @param string $path       The requested path.
	 * @param string $title      The requested site title.
	 * @param string $user       The user's requested login name.
	 * @param string $user_email The user's email address.
	 * @param string $key        The user's activation key.
	 * @param array  $meta       Signup meta data. By default, contains the requested privacy setting and lang_id.
	 */
	do_action( 'after_signup_site', $domain, $path, $title, $user, $user_email, $key, $meta );
}

/**
 * Records user signup information for future activation.
 *
 * This function is used when user registration is open but
 * new site registration is not.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $user       The user's requested login name.
 * @param string $user_email The user's email address.
 * @param array  $meta       Optional. Signup meta data. Default empty array.
 */
function wpmu_signup_user( $user, $user_email, $meta = array() ) {
	global $wpdb;

	// Format data.
	$user       = preg_replace( '/\s+/', '', sanitize_user( $user, true ) );
	$user_email = sanitize_email( $user_email );
	$key        = substr( md5( time() . wp_rand() . $user_email ), 0, 16 );

	/**
	 * Filters the metadata for a user signup.
	 *
	 * The metadata will be serialized prior to storing it in the database.
	 *
	 * @since 4.8.0
	 *
	 * @param array  $meta       Signup meta data. Default empty array.
	 * @param string $user       The user's requested login name.
	 * @param string $user_email The user's email address.
	 * @param string $key        The user's activation key.
	 */
	$meta = apply_filters( 'signup_user_meta', $meta, $user, $user_email, $key );

	$wpdb->insert(
		$wpdb->signups,
		array(
			'domain'         => '',
			'path'           => '',
			'title'          => '',
			'user_login'     => $user,
			'user_email'     => $user_email,
			'registered'     => current_time( 'mysql', true ),
			'activation_key' => $key,
			'meta'           => serialize( $meta ),
		)
	);

	/**
	 * Fires after a user's signup information has been written to the database.
	 *
	 * @since 4.4.0
	 *
	 * @param string $user       The user's requested login name.
	 * @param string $user_email The user's email address.
	 * @param string $key        The user's activation key.
	 * @param array  $meta       Signup meta data. Default empty array.
	 */
	do_action( 'after_signup_user', $user, $user_email, $key, $meta );
}

/**
 * Sends a confirmation request email to a user when they sign up for a new site. The new site will not become active
 * until the confirmation link is clicked.
 *
 * This is the notification function used when site registration
 * is enabled.
 *
 * Filter {@see 'wpmu_signup_blog_notification'} to bypass this function or
 * replace it with your own notification behavior.
 *
 * Filter {@see 'wpmu_signup_blog_notification_email'} and
 * {@see 'wpmu_signup_blog_notification_subject'} to change the content
 * and subject line of the email sent to newly registered users.
 *
 * @since MU (3.0.0)
 *
 * @param string $domain     The new blog domain.
 * @param string $path       The new blog path.
 * @param string $title      The site title.
 * @param string $user_login The user's login name.
 * @param string $user_email The user's email address.
 * @param string $key        The activation key created in wpmu_signup_blog().
 * @param array  $meta       Optional. Signup meta data. By default, contains the requested privacy setting and lang_id.
 * @return bool
 */
function wpmu_signup_blog_notification(
	$domain,
	$path,
	$title,
	$user_login,
	$user_email,
	#[\SensitiveParameter]
	$key,
	$meta = array()
) {
	/**
	 * Filters whether to bypass the new site email notification.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string|false $domain     Site domain, or false to prevent the email from sending.
	 * @param string       $path       Site path.
	 * @param string       $title      Site title.
	 * @param string       $user_login User login name.
	 * @param string       $user_email User email address.
	 * @param string       $key        Activation key created in wpmu_signup_blog().
	 * @param array        $meta       Signup meta data. By default, contains the requested privacy setting and lang_id.
	 */
	if ( ! apply_filters( 'wpmu_signup_blog_notification', $domain, $path, $title, $user_login, $user_email, $key, $meta ) ) {
		return false;
	}

	// Send email with activation link.
	if ( ! is_subdomain_install() || get_current_network_id() !== 1 ) {
		$activate_url = network_site_url( "wp-activate.php?key=$key" );
	} else {
		$activate_url = "http://{$domain}{$path}wp-activate.php?key=$key"; // @todo Use *_url() API.
	}

	$activate_url = esc_url( $activate_url );

	$admin_email = get_site_option( 'admin_email' );

	if ( '' === $admin_email ) {
		$admin_email = 'support@' . wp_parse_url( network_home_url(), PHP_URL_HOST );
	}

	$from_name       = ( '' !== get_site_option( 'site_name' ) ) ? esc_html( get_site_option( 'site_name' ) ) : 'WordPress';
	$message_headers = "From: \"{$from_name}\" <{$admin_email}>\n" . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";

	$user            = get_user_by( 'login', $user_login );
	$switched_locale = $user && switch_to_user_locale( $user->ID );

	$message = sprintf(
		/**
		 * Filters the message content of the new blog notification email.
		 *
		 * Content should be formatted for transmission via wp_mail().
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string $content    Content of the notification email.
		 * @param string $domain     Site domain.
		 * @param string $path       Site path.
		 * @param string $title      Site title.
		 * @param string $user_login User login name.
		 * @param string $user_email User email address.
		 * @param string $key        Activation key created in wpmu_signup_blog().
		 * @param array  $meta       Signup meta data. By default, contains the requested privacy setting and lang_id.
		 */
		apply_filters(
			'wpmu_signup_blog_notification_email',
			/* translators: New site notification email. 1: Activation URL, 2: New site URL. */
			__( "To activate your site, please click the following link:\n\n%1\$s\n\nAfter you activate, you will receive *another email* with your login.\n\nAfter you activate, you can visit your site here:\n\n%2\$s" ),
			$domain,
			$path,
			$title,
			$user_login,
			$user_email,
			$key,
			$meta
		),
		$activate_url,
		esc_url( "http://{$domain}{$path}" ),
		$key
	);

	$subject = sprintf(
		/**
		 * Filters the subject of the new blog notification email.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string $subject    Subject of the notification email.
		 * @param string $domain     Site domain.
		 * @param string $path       Site path.
		 * @param string $title      Site title.
		 * @param string $user_login User login name.
		 * @param string $user_email User email address.
		 * @param string $key        Activation key created in wpmu_signup_blog().
		 * @param array  $meta       Signup meta data. By default, contains the requested privacy setting and lang_id.
		 */
		apply_filters(
			'wpmu_signup_blog_notification_subject',
			/* translators: New site notification email subject. 1: Network title, 2: New site URL. */
			_x( '[%1$s] Activate %2$s', 'New site notification email subject' ),
			$domain,
			$path,
			$title,
			$user_login,
			$user_email,
			$key,
			$meta
		),
		$from_name,
		esc_url( 'http://' . $domain . $path )
	);

	wp_mail( $user_email, wp_specialchars_decode( $subject ), $message, $message_headers );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	return true;
}

/**
 * Sends a confirmation request email to a user when they sign up for a new user account (without signing up for a site
 * at the same time). The user account will not become active until the confirmation link is clicked.
 *
 * This is the notification function used when no new site has
 * been requested.
 *
 * Filter {@see 'wpmu_signup_user_notification'} to bypass this function or
 * replace it with your own notification behavior.
 *
 * Filter {@see 'wpmu_signup_user_notification_email'} and
 * {@see 'wpmu_signup_user_notification_subject'} to change the content
 * and subject line of the email sent to newly registered users.
 *
 * @since MU (3.0.0)
 *
 * @param string $user_login The user's login name.
 * @param string $user_email The user's email address.
 * @param string $key        The activation key created in wpmu_signup_user()
 * @param array  $meta       Optional. Signup meta data. Default empty array.
 * @return bool
 */
function wpmu_signup_user_notification(
	$user_login,
	$user_email,
	#[\SensitiveParameter]
	$key,
	$meta = array()
) {
	/**
	 * Filters whether to bypass the email notification for new user sign-up.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $user_login User login name.
	 * @param string $user_email User email address.
	 * @param string $key        Activation key created in wpmu_signup_user().
	 * @param array  $meta       Signup meta data. Default empty array.
	 */
	if ( ! apply_filters( 'wpmu_signup_user_notification', $user_login, $user_email, $key, $meta ) ) {
		return false;
	}

	$user            = get_user_by( 'login', $user_login );
	$switched_locale = $user && switch_to_user_locale( $user->ID );

	// Send email with activation link.
	$admin_email = get_site_option( 'admin_email' );

	if ( '' === $admin_email ) {
		$admin_email = 'support@' . wp_parse_url( network_home_url(), PHP_URL_HOST );
	}

	$from_name       = ( '' !== get_site_option( 'site_name' ) ) ? esc_html( get_site_option( 'site_name' ) ) : 'WordPress';
	$message_headers = "From: \"{$from_name}\" <{$admin_email}>\n" . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
	$message         = sprintf(
		/**
		 * Filters the content of the notification email for new user sign-up.
		 *
		 * Content should be formatted for transmission via wp_mail().
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string $content    Content of the notification email.
		 * @param string $user_login User login name.
		 * @param string $user_email User email address.
		 * @param string $key        Activation key created in wpmu_signup_user().
		 * @param array  $meta       Signup meta data. Default empty array.
		 */
		apply_filters(
			'wpmu_signup_user_notification_email',
			/* translators: New user notification email. %s: Activation URL. */
			__( "To activate your user, please click the following link:\n\n%s\n\nAfter you activate, you will receive *another email* with your login." ),
			$user_login,
			$user_email,
			$key,
			$meta
		),
		site_url( "wp-activate.php?key=$key" )
	);

	$subject = sprintf(
		/**
		 * Filters the subject of the notification email of new user signup.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param string $subject    Subject of the notification email.
		 * @param string $user_login User login name.
		 * @param string $user_email User email address.
		 * @param string $key        Activation key created in wpmu_signup_user().
		 * @param array  $meta       Signup meta data. Default empty array.
		 */
		apply_filters(
			'wpmu_signup_user_notification_subject',
			/* translators: New user notification email subject. 1: Network title, 2: New user login. */
			_x( '[%1$s] Activate %2$s', 'New user notification email subject' ),
			$user_login,
			$user_email,
			$key,
			$meta
		),
		$from_name,
		$user_login
	);

	wp_mail( $user_email, wp_specialchars_decode( $subject ), $message, $message_headers );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	return true;
}

/**
 * Activates a signup.
 *
 * Hook to {@see 'wpmu_activate_user'} or {@see 'wpmu_activate_blog'} for events
 * that should happen only when users or sites are self-created (since
 * those actions are not called when users and sites are created
 * by a Super Admin).
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $key The activation key provided to the user.
 * @return array|WP_Error An array containing information about the activated user and/or blog.
 */
function wpmu_activate_signup(
	#[\SensitiveParameter]
	$key
) {
	global $wpdb;

	$signup = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->signups WHERE activation_key = %s", $key ) );

	if ( empty( $signup ) ) {
		return new WP_Error( 'invalid_key', __( 'Invalid activation key.' ) );
	}

	if ( $signup->active ) {
		if ( empty( $signup->domain ) ) {
			return new WP_Error( 'already_active', __( 'The user is already active.' ), $signup );
		} else {
			return new WP_Error( 'already_active', __( 'The site is already active.' ), $signup );
		}
	}

	$meta     = maybe_unserialize( $signup->meta );
	$password = wp_generate_password( 12, false );

	$user_id = username_exists( $signup->user_login );

	if ( ! $user_id ) {
		$user_id = wpmu_create_user( $signup->user_login, $password, $signup->user_email );
	} else {
		$user_already_exists = true;
	}

	if ( ! $user_id ) {
		return new WP_Error( 'create_user', __( 'Could not create user' ), $signup );
	}

	$now = current_time( 'mysql', true );

	if ( empty( $signup->domain ) ) {
		$wpdb->update(
			$wpdb->signups,
			array(
				'active'    => 1,
				'activated' => $now,
			),
			array( 'activation_key' => $key )
		);

		if ( isset( $user_already_exists ) ) {
			return new WP_Error( 'user_already_exists', __( 'That username is already activated.' ), $signup );
		}

		/**
		 * Fires immediately after a new user is activated.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param int    $user_id  User ID.
		 * @param string $password User password.
		 * @param array  $meta     Signup meta data.
		 */
		do_action( 'wpmu_activate_user', $user_id, $password, $meta );

		return array(
			'user_id'  => $user_id,
			'password' => $password,
			'meta'     => $meta,
		);
	}

	$blog_id = wpmu_create_blog( $signup->domain, $signup->path, $signup->title, $user_id, $meta, get_current_network_id() );

	// TODO: What to do if we create a user but cannot create a blog?
	if ( is_wp_error( $blog_id ) ) {
		/*
		 * If blog is taken, that means a previous attempt to activate this blog
		 * failed in between creating the blog and setting the activation flag.
		 * Let's just set the active flag and instruct the user to reset their password.
		 */
		if ( 'blog_taken' === $blog_id->get_error_code() ) {
			$blog_id->add_data( $signup );
			$wpdb->update(
				$wpdb->signups,
				array(
					'active'    => 1,
					'activated' => $now,
				),
				array( 'activation_key' => $key )
			);
		}
		return $blog_id;
	}

	$wpdb->update(
		$wpdb->signups,
		array(
			'active'    => 1,
			'activated' => $now,
		),
		array( 'activation_key' => $key )
	);

	/**
	 * Fires immediately after a site is activated.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param int    $blog_id       Blog ID.
	 * @param int    $user_id       User ID.
	 * @param string $password      User password.
	 * @param string $signup_title  Site title.
	 * @param array  $meta          Signup meta data. By default, contains the requested privacy setting and lang_id.
	 */
	do_action( 'wpmu_activate_blog', $blog_id, $user_id, $password, $signup->title, $meta );

	return array(
		'blog_id'  => $blog_id,
		'user_id'  => $user_id,
		'password' => $password,
		'title'    => $signup->title,
		'meta'     => $meta,
	);
}

/**
 * Deletes an associated signup entry when a user is deleted from the database.
 *
 * @since 5.5.0
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int      $id       ID of the user to delete.
 * @param int|null $reassign ID of the user to reassign posts and links to.
 * @param WP_User  $user     User object.
 */
function wp_delete_signup_on_user_delete( $id, $reassign, $user ) {
	global $wpdb;

	$wpdb->delete( $wpdb->signups, array( 'user_login' => $user->user_login ) );
}

/**
 * Creates a user.
 *
 * This function runs when a user self-registers as well as when
 * a Super Admin creates a new user. Hook to {@see 'wpmu_new_user'} for events
 * that should affect all new users, but only on Multisite (otherwise
 * use {@see 'user_register'}).
 *
 * @since MU (3.0.0)
 *
 * @param string $user_name The new user's login name.
 * @param string $password  The new user's password.
 * @param string $email     The new user's email address.
 * @return int|false Returns false on failure, or int $user_id on success.
 */
function wpmu_create_user(
	$user_name,
	#[\SensitiveParameter]
	$password,
	$email
) {
	$user_name = preg_replace( '/\s+/', '', sanitize_user( $user_name, true ) );

	$user_id = wp_create_user( $user_name, $password, $email );
	if ( is_wp_error( $user_id ) ) {
		return false;
	}

	// Newly created users have no roles or caps until they are added to a blog.
	delete_user_option( $user_id, 'capabilities' );
	delete_user_option( $user_id, 'user_level' );

	/**
	 * Fires immediately after a new user is created.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param int $user_id User ID.
	 */
	do_action( 'wpmu_new_user', $user_id );

	return $user_id;
}

/**
 * Creates a site.
 *
 * This function runs when a user self-registers a new site as well
 * as when a Super Admin creates a new site. Hook to {@see 'wpmu_new_blog'}
 * for events that should affect all new sites.
 *
 * On subdirectory installations, $domain is the same as the main site's
 * domain, and the path is the subdirectory name (eg 'example.com'
 * and '/blog1/'). On subdomain installations, $domain is the new subdomain +
 * root domain (eg 'blog1.example.com'), and $path is '/'.
 *
 * @since MU (3.0.0)
 *
 * @param string $domain     The new site's domain.
 * @param string $path       The new site's path.
 * @param string $title      The new site's title.
 * @param int    $user_id    The user ID of the new site's admin.
 * @param array  $options    Optional. Array of key=>value pairs used to set initial site options.
 *                           If valid status keys are included ('public', 'archived', 'mature',
 *                           'spam', 'deleted', or 'lang_id') the given site status(es) will be
 *                           updated. Otherwise, keys and values will be used to set options for
 *                           the new site. Default empty array.
 * @param int    $network_id Optional. Network ID. Only relevant on multi-network installations.
 *                           Default 1.
 * @return int|WP_Error Returns WP_Error object on failure, the new site ID on success.
 */
function wpmu_create_blog( $domain, $path, $title, $user_id, $options = array(), $network_id = 1 ) {
	$defaults = array(
		'public' => 0,
	);
	$options  = wp_parse_args( $options, $defaults );

	$title   = strip_tags( $title );
	$user_id = (int) $user_id;

	// Check if the domain has been used already. We should return an error message.
	if ( domain_exists( $domain, $path, $network_id ) ) {
		return new WP_Error( 'blog_taken', __( 'Sorry, that site already exists!' ) );
	}

	if ( ! wp_installing() ) {
		wp_installing( true );
	}

	$allowed_data_fields = array( 'public', 'archived', 'mature', 'spam', 'deleted', 'lang_id' );

	$site_data = array_merge(
		array(
			'domain'     => $domain,
			'path'       => $path,
			'network_id' => $network_id,
		),
		array_intersect_key( $options, array_flip( $allowed_data_fields ) )
	);

	// Data to pass to wp_initialize_site().
	$site_initialization_data = array(
		'title'   => $title,
		'user_id' => $user_id,
		'options' => array_diff_key( $options, array_flip( $allowed_data_fields ) ),
	);

	$blog_id = wp_insert_site( array_merge( $site_data, $site_initialization_data ) );

	if ( is_wp_error( $blog_id ) ) {
		return $blog_id;
	}

	wp_cache_set_sites_last_changed();

	return $blog_id;
}

/**
 * Notifies the network admin that a new site has been activated.
 *
 * Filter {@see 'newblog_notify_siteadmin'} to change the content of
 * the notification email.
 *
 * @since MU (3.0.0)
 * @since 5.1.0 $blog_id now supports input from the {@see 'wp_initialize_site'} action.
 *
 * @param WP_Site|int $blog_id    The new site's object or ID.
 * @param string      $deprecated Not used.
 * @return bool
 */
function newblog_notify_siteadmin( $blog_id, $deprecated = '' ) {
	if ( is_object( $blog_id ) ) {
		$blog_id = $blog_id->blog_id;
	}

	if ( 'yes' !== get_site_option( 'registrationnotification' ) ) {
		return false;
	}

	$email = get_site_option( 'admin_email' );

	if ( ! is_email( $email ) ) {
		return false;
	}

	$options_site_url = esc_url( network_admin_url( 'settings.php' ) );

	switch_to_blog( $blog_id );
	$blogname = get_option( 'blogname' );
	$siteurl  = site_url();
	restore_current_blog();

	$msg = sprintf(
		/* translators: New site notification email. 1: Site URL, 2: User IP address, 3: URL to Network Settings screen. */
		__(
			'New Site: %1$s
URL: %2$s
Remote IP address: %3$s

Disable these notifications: %4$s'
		),
		$blogname,
		$siteurl,
		wp_unslash( $_SERVER['REMOTE_ADDR'] ),
		$options_site_url
	);
	/**
	 * Filters the message body of the new site activation email sent
	 * to the network administrator.
	 *
	 * @since MU (3.0.0)
	 * @since 5.4.0 The `$blog_id` parameter was added.
	 *
	 * @param string     $msg     Email body.
	 * @param int|string $blog_id The new site's ID as an integer or numeric string.
	 */
	$msg = apply_filters( 'newblog_notify_siteadmin', $msg, $blog_id );

	/* translators: New site notification email subject. %s: New site URL. */
	wp_mail( $email, sprintf( __( 'New Site Registration: %s' ), $siteurl ), $msg );

	return true;
}

/**
 * Notifies the network admin that a new user has been activated.
 *
 * Filter {@see 'newuser_notify_siteadmin'} to change the content of
 * the notification email.
 *
 * @since MU (3.0.0)
 *
 * @param int $user_id The new user's ID.
 * @return bool
 */
function newuser_notify_siteadmin( $user_id ) {
	if ( 'yes' !== get_site_option( 'registrationnotification' ) ) {
		return false;
	}

	$email = get_site_option( 'admin_email' );

	if ( ! is_email( $email ) ) {
		return false;
	}

	$user = get_userdata( $user_id );

	$options_site_url = esc_url( network_admin_url( 'settings.php' ) );

	$msg = sprintf(
		/* translators: New user notification email. 1: User login, 2: User IP address, 3: URL to Network Settings screen. */
		__(
			'New User: %1$s
Remote IP address: %2$s

Disable these notifications: %3$s'
		),
		$user->user_login,
		wp_unslash( $_SERVER['REMOTE_ADDR'] ),
		$options_site_url
	);

	/**
	 * Filters the message body of the new user activation email sent
	 * to the network administrator.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string  $msg  Email body.
	 * @param WP_User $user WP_User instance of the new user.
	 */
	$msg = apply_filters( 'newuser_notify_siteadmin', $msg, $user );

	/* translators: New user notification email subject. %s: User login. */
	wp_mail( $email, sprintf( __( 'New User Registration: %s' ), $user->user_login ), $msg );

	return true;
}

/**
 * Checks whether a site name is already taken.
 *
 * The name is the site's subdomain or the site's subdirectory
 * path depending on the network settings.
 *
 * Used during the new site registration process to ensure
 * that each site name is unique.
 *
 * @since MU (3.0.0)
 *
 * @param string $domain     The domain to be checked.
 * @param string $path       The path to be checked.
 * @param int    $network_id Optional. Network ID. Only relevant on multi-network installations.
 *                           Default 1.
 * @return int|null The site ID if the site name exists, null otherwise.
 */
function domain_exists( $domain, $path, $network_id = 1 ) {
	$path   = trailingslashit( $path );
	$args   = array(
		'network_id'             => $network_id,
		'domain'                 => $domain,
		'path'                   => $path,
		'fields'                 => 'ids',
		'number'                 => 1,
		'update_site_meta_cache' => false,
	);
	$result = get_sites( $args );
	$result = array_shift( $result );

	/**
	 * Filters whether a site name is taken.
	 *
	 * The name is the site's subdomain or the site's subdirectory
	 * path depending on the network settings.
	 *
	 * @since 3.5.0
	 *
	 * @param int|null $result     The site ID if the site name exists, null otherwise.
	 * @param string   $domain     Domain to be checked.
	 * @param string   $path       Path to be checked.
	 * @param int      $network_id Network ID. Only relevant on multi-network installations.
	 */
	return apply_filters( 'domain_exists', $result, $domain, $path, $network_id );
}

/**
 * Notifies the site administrator that their site activation was successful.
 *
 * Filter {@see 'wpmu_welcome_notification'} to disable or bypass.
 *
 * Filter {@see 'update_welcome_email'} and {@see 'update_welcome_subject'} to
 * modify the content and subject line of the notification email.
 *
 * @since MU (3.0.0)
 *
 * @param int    $blog_id  Site ID.
 * @param int    $user_id  User ID.
 * @param string $password User password, or "N/A" if the user account is not new.
 * @param string $title    Site title.
 * @param array  $meta     Optional. Signup meta data. By default, contains the requested privacy setting and lang_id.
 * @return bool Whether the email notification was sent.
 */
function wpmu_welcome_notification(
	$blog_id,
	$user_id,
	#[\SensitiveParameter]
	$password,
	$title,
	$meta = array()
) {
	$current_network = get_network();

	/**
	 * Filters whether to bypass the welcome email sent to the site administrator after site activation.
	 *
	 * Returning false disables the welcome email.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param int|false $blog_id  Site ID, or false to prevent the email from sending.
	 * @param int       $user_id  User ID of the site administrator.
	 * @param string    $password User password, or "N/A" if the user account is not new.
	 * @param string    $title    Site title.
	 * @param array     $meta     Signup meta data. By default, contains the requested privacy setting and lang_id.
	 */
	if ( ! apply_filters( 'wpmu_welcome_notification', $blog_id, $user_id, $password, $title, $meta ) ) {
		return false;
	}

	$user = get_userdata( $user_id );

	$switched_locale = switch_to_user_locale( $user_id );

	$welcome_email = get_site_option( 'welcome_email' );

	if ( ! $welcome_email ) {
		/* translators: Do not translate USERNAME, SITE_NAME, BLOG_URL, PASSWORD: those are placeholders. */
		$welcome_email = __(
			'Howdy USERNAME,

Your new SITE_NAME site has been successfully set up at:
BLOG_URL

You can log in to the administrator account with the following information:

Username: USERNAME
Password: PASSWORD
Log in here: BLOG_URLwp-login.php

We hope you enjoy your new site. Thanks!

--The Team @ SITE_NAME'
		);
	}

	$url = get_blogaddress_by_id( $blog_id );

	$welcome_email = str_replace( 'SITE_NAME', $current_network->site_name, $welcome_email );
	$welcome_email = str_replace( 'BLOG_TITLE', $title, $welcome_email );
	$welcome_email = str_replace( 'BLOG_URL', $url, $welcome_email );
	$welcome_email = str_replace( 'USERNAME', $user->user_login, $welcome_email );
	$welcome_email = str_replace( 'PASSWORD', $password, $welcome_email );

	/**
	 * Filters the content of the welcome email sent to the site administrator after site activation.
	 *
	 * Content should be formatted for transmission via wp_mail().
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $welcome_email Message body of the email.
	 * @param int    $blog_id       Site ID.
	 * @param int    $user_id       User ID of the site administrator.
	 * @param string $password      User password, or "N/A" if the user account is not new.
	 * @param string $title         Site title.
	 * @param array  $meta          Signup meta data. By default, contains the requested privacy setting and lang_id.
	 */
	$welcome_email = apply_filters( 'update_welcome_email', $welcome_email, $blog_id, $user_id, $password, $title, $meta );

	$admin_email = get_site_option( 'admin_email' );

	if ( '' === $admin_email ) {
		$admin_email = 'support@' . wp_parse_url( network_home_url(), PHP_URL_HOST );
	}

	$from_name       = ( '' !== get_site_option( 'site_name' ) ) ? esc_html( get_site_option( 'site_name' ) ) : 'WordPress';
	$message_headers = "From: \"{$from_name}\" <{$admin_email}>\n" . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
	$message         = $welcome_email;

	if ( empty( $current_network->site_name ) ) {
		$current_network->site_name = 'WordPress';
	}

	/* translators: New site notification email subject. 1: Network title, 2: New site title. */
	$subject = __( 'New %1$s Site: %2$s' );

	/**
	 * Filters the subject of the welcome email sent to the site administrator after site activation.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $subject Subject of the email.
	 */
	$subject = apply_filters( 'update_welcome_subject', sprintf( $subject, $current_network->site_name, wp_unslash( $title ) ) );

	wp_mail( $user->user_email, wp_specialchars_decode( $subject ), $message, $message_headers );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	return true;
}

/**
 * Notifies the Multisite network administrator that a new site was created.
 *
 * Filter {@see 'send_new_site_email'} to disable or bypass.
 *
 * Filter {@see 'new_site_email'} to filter the contents.
 *
 * @since 5.6.0
 *
 * @param int $site_id Site ID of the new site.
 * @param int $user_id User ID of the administrator of the new site.
 * @return bool Whether the email notification was sent.
 */
function wpmu_new_site_admin_notification( $site_id, $user_id ) {
	$site  = get_site( $site_id );
	$user  = get_userdata( $user_id );
	$email = get_site_option( 'admin_email' );

	if ( ! $site || ! $user || ! $email ) {
		return false;
	}

	/**
	 * Filters whether to send an email to the Multisite network administrator when a new site is created.
	 *
	 * Return false to disable sending the email.
	 *
	 * @since 5.6.0
	 *
	 * @param bool    $send Whether to send the email.
	 * @param WP_Site $site Site object of the new site.
	 * @param WP_User $user User object of the administrator of the new site.
	 */
	if ( ! apply_filters( 'send_new_site_email', true, $site, $user ) ) {
		return false;
	}

	$switched_locale = false;
	$network_admin   = get_user_by( 'email', $email );

	if ( $network_admin ) {
		// If the network admin email address corresponds to a user, switch to their locale.
		$switched_locale = switch_to_user_locale( $network_admin->ID );
	} else {
		// Otherwise switch to the locale of the current site.
		$switched_locale = switch_to_locale( get_locale() );
	}

	$subject = sprintf(
		/* translators: New site notification email subject. %s: Network title. */
		__( '[%s] New Site Created' ),
		get_network()->site_name
	);

	$message = sprintf(
		/* translators: New site notification email. 1: User login, 2: Site URL, 3: Site title. */
		__(
			'New site created by %1$s

Address: %2$s
Name: %3$s'
		),
		$user->user_login,
		get_site_url( $site->id ),
		get_blog_option( $site->id, 'blogname' )
	);

	$header = sprintf(
		'From: "%1$s" <%2$s>',
		_x( 'Site Admin', 'email "From" field' ),
		$email
	);

	$new_site_email = array(
		'to'      => $email,
		'subject' => $subject,
		'message' => $message,
		'headers' => $header,
	);

	/**
	 * Filters the content of the email sent to the Multisite network administrator when a new site is created.
	 *
	 * Content should be formatted for transmission via wp_mail().
	 *
	 * @since 5.6.0
	 *
	 * @param array $new_site_email {
	 *     Used to build wp_mail().
	 *
	 *     @type string $to      The email address of the recipient.
	 *     @type string $subject The subject of the email.
	 *     @type string $message The content of the email.
	 *     @type string $headers Headers.
	 * }
	 * @param WP_Site $site         Site object of the new site.
	 * @param WP_User $user         User object of the administrator of the new site.
	 */
	$new_site_email = apply_filters( 'new_site_email', $new_site_email, $site, $user );

	wp_mail(
		$new_site_email['to'],
		wp_specialchars_decode( $new_site_email['subject'] ),
		$new_site_email['message'],
		$new_site_email['headers']
	);

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	return true;
}

/**
 * Notifies a user that their account activation has been successful.
 *
 * Filter {@see 'wpmu_welcome_user_notification'} to disable or bypass.
 *
 * Filter {@see 'update_welcome_user_email'} and {@see 'update_welcome_user_subject'} to
 * modify the content and subject line of the notification email.
 *
 * @since MU (3.0.0)
 *
 * @param int    $user_id  User ID.
 * @param string $password User password.
 * @param array  $meta     Optional. Signup meta data. Default empty array.
 * @return bool
 */
function wpmu_welcome_user_notification(
	$user_id,
	#[\SensitiveParameter]
	$password,
	$meta = array()
) {
	$current_network = get_network();

	/**
	 * Filters whether to bypass the welcome email after user activation.
	 *
	 * Returning false disables the welcome email.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param int    $user_id  User ID.
	 * @param string $password User password.
	 * @param array  $meta     Signup meta data. Default empty array.
	 */
	if ( ! apply_filters( 'wpmu_welcome_user_notification', $user_id, $password, $meta ) ) {
		return false;
	}

	$welcome_email = get_site_option( 'welcome_user_email' );

	$user = get_userdata( $user_id );

	$switched_locale = switch_to_user_locale( $user_id );

	/**
	 * Filters the content of the welcome email after user activation.
	 *
	 * Content should be formatted for transmission via wp_mail().
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $welcome_email The message body of the account activation success email.
	 * @param int    $user_id       User ID.
	 * @param string $password      User password.
	 * @param array  $meta          Signup meta data. Default empty array.
	 */
	$welcome_email = apply_filters( 'update_welcome_user_email', $welcome_email, $user_id, $password, $meta );
	$welcome_email = str_replace( 'SITE_NAME', $current_network->site_name, $welcome_email );
	$welcome_email = str_replace( 'USERNAME', $user->user_login, $welcome_email );
	$welcome_email = str_replace( 'PASSWORD', $password, $welcome_email );
	$welcome_email = str_replace( 'LOGINLINK', wp_login_url(), $welcome_email );

	$admin_email = get_site_option( 'admin_email' );

	if ( '' === $admin_email ) {
		$admin_email = 'support@' . wp_parse_url( network_home_url(), PHP_URL_HOST );
	}

	$from_name       = ( '' !== get_site_option( 'site_name' ) ) ? esc_html( get_site_option( 'site_name' ) ) : 'WordPress';
	$message_headers = "From: \"{$from_name}\" <{$admin_email}>\n" . 'Content-Type: text/plain; charset="' . get_option( 'blog_charset' ) . "\"\n";
	$message         = $welcome_email;

	if ( empty( $current_network->site_name ) ) {
		$current_network->site_name = 'WordPress';
	}

	/* translators: New user notification email subject. 1: Network title, 2: New user login. */
	$subject = __( 'New %1$s User: %2$s' );

	/**
	 * Filters the subject of the welcome email after user activation.
	 *
	 * @since MU (3.0.0)
	 *
	 * @param string $subject Subject of the email.
	 */
	$subject = apply_filters( 'update_welcome_user_subject', sprintf( $subject, $current_network->site_name, $user->user_login ) );

	wp_mail( $user->user_email, wp_specialchars_decode( $subject ), $message, $message_headers );

	if ( $switched_locale ) {
		restore_previous_locale();
	}

	return true;
}

/**
 * Gets the current network.
 *
 * Returns an object containing the 'id', 'domain', 'path', and 'site_name'
 * properties of the network being viewed.
 *
 * @see wpmu_current_site()
 *
 * @since MU (3.0.0)
 *
 * @global WP_Network $current_site The current network.
 *
 * @return WP_Network The current network.
 */
function get_current_site() {
	global $current_site;
	return $current_site;
}

/**
 * Gets a user's most recent post.
 *
 * Walks through each of a user's blogs to find the post with
 * the most recent post_date_gmt.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param int $user_id User ID.
 * @return array Contains the blog_id, post_id, post_date_gmt, and post_gmt_ts.
 */
function get_most_recent_post_of_user( $user_id ) {
	global $wpdb;

	$user_blogs       = get_blogs_of_user( (int) $user_id );
	$most_recent_post = array();

	/*
	 * Walk through each blog and get the most recent post
	 * published by $user_id.
	 */
	foreach ( (array) $user_blogs as $blog ) {
		$prefix      = $wpdb->get_blog_prefix( $blog->userblog_id );
		$recent_post = $wpdb->get_row( $wpdb->prepare( "SELECT ID, post_date_gmt FROM {$prefix}posts WHERE post_author = %d AND post_type = 'post' AND post_status = 'publish' ORDER BY post_date_gmt DESC LIMIT 1", $user_id ), ARRAY_A );

		// Make sure we found a post.
		if ( isset( $recent_post['ID'] ) ) {
			$post_gmt_ts = strtotime( $recent_post['post_date_gmt'] );

			/*
			 * If this is the first post checked
			 * or if this post is newer than the current recent post,
			 * make it the new most recent post.
			 */
			if ( ! isset( $most_recent_post['post_gmt_ts'] ) || ( $post_gmt_ts > $most_recent_post['post_gmt_ts'] ) ) {
				$most_recent_post = array(
					'blog_id'       => $blog->userblog_id,
					'post_id'       => $recent_post['ID'],
					'post_date_gmt' => $recent_post['post_date_gmt'],
					'post_gmt_ts'   => $post_gmt_ts,
				);
			}
		}
	}

	return $most_recent_post;
}

//
// Misc functions.
//

/**
 * Checks an array of MIME types against a list of allowed types.
 *
 * WordPress ships with a set of allowed upload filetypes,
 * which is defined in wp-includes/functions.php in
 * get_allowed_mime_types(). This function is used to filter
 * that list against the filetypes allowed provided by Multisite
 * Super Admins at wp-admin/network/settings.php.
 *
 * @since MU (3.0.0)
 *
 * @param array $mimes
 * @return array
 */
function check_upload_mimes( $mimes ) {
	$site_exts  = explode( ' ', get_site_option( 'upload_filetypes', 'jpg jpeg png gif' ) );
	$site_mimes = array();
	foreach ( $site_exts as $ext ) {
		foreach ( $mimes as $ext_pattern => $mime ) {
			if ( '' !== $ext && str_contains( $ext_pattern, $ext ) ) {
				$site_mimes[ $ext_pattern ] = $mime;
			}
		}
	}
	return $site_mimes;
}

/**
 * Updates a blog's post count.
 *
 * WordPress MS stores a blog's post count as an option so as
 * to avoid extraneous COUNTs when a blog's details are fetched
 * with get_site(). This function is called when posts are published
 * or unpublished to make sure the count stays current.
 *
 * @since MU (3.0.0)
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param string $deprecated Not used.
 */
function update_posts_count( $deprecated = '' ) {
	global $wpdb;
	update_option( 'post_count', (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_status = 'publish' and post_type = 'post'" ), true );
}

/**
 * Logs the user email, IP, and registration date of a new site.
 *
 * @since MU (3.0.0)
 * @since 5.1.0 Parameters now support input from the {@see 'wp_initialize_site'} action.
 *
 * @global wpdb $wpdb WordPress database abstraction object.
 *
 * @param WP_Site|int $blog_id The new site's object or ID.
 * @param int|array   $user_id User ID, or array of arguments including 'user_id'.
 */
function wpmu_log_new_registrations( $blog_id, $user_id ) {
	global $wpdb;

	if ( is_object( $blog_id ) ) {
		$blog_id = $blog_id->blog_id;
	}

	if ( is_array( $user_id ) ) {
		$user_id = ! empty( $user_id['user_id'] ) ? $user_id['user_id'] : 0;
	}

	$user = get_userdata( (int) $user_id );
	if ( $user ) {
		$wpdb->insert(
			$wpdb->registration_log,
			array(
				'email'           => $user->user_email,
				'IP'              => preg_replace( '/[^0-9., ]/', '', wp_unslash( $_SERVER['REMOTE_ADDR'] ) ),
				'blog_id'         => $blog_id,
				'date_registered' => current_time( 'mysql' ),
			)
		);
	}
}

/**
 * Ensures that the current site's domain is listed in the allowed redirect host list.
 *
 * @see wp_validate_redirect()
 * @since MU (3.0.0)
 *
 * @param array|string $deprecated Not used.
 * @return string[] {
 *     An array containing the current site's domain.
 *
 *     @type string $0 The current site's domain.
 * }
 */
function redirect_this_site( $deprecated = '' ) {
	return array( get_network()->domain );
}

/**
 * Checks whether an upload is too big.
 *
 * @since MU (3.0.0)
 *
 * @param array $upload An array of information about the newly-uploaded file.
 * @return string|array If the upload is under the size limit, $upload is returned. Otherwise returns an error message.
 */
function upload_is_file_too_big( $upload ) {
	if ( ! is_array( $upload ) || defined( 'WP_IMPORTING' ) || get_site_option( 'upload_space_check_disabled' ) ) {
		return $upload;
	}

	if ( strlen( $upload['bits'] ) > ( KB_IN_BYTES * get_site_option( 'fileupload_maxk', 1500 ) ) ) {
		/* translators: %s: Maximum allowed file size in kilobytes. */
		return sprintf( __( 'This file is too big. Files must be less than %s KB in size.' ) . '<br />', get_site_option( 'fileupload_maxk', 1500 ) );
	}

	return $upload;
}

/**
 * Adds a nonce field to the signup page.
 *
 * @since MU (3.0.0)
 */
function signup_nonce_fields() {
	$id = mt_rand();
	echo "<input type='hidden' name='signup_form_id' value='{$id}' />";
	wp_nonce_field( 'signup_form_' . $id, '_signup_form', false );
}

/**
 * Processes the signup nonce created in signup_nonce_fields().
 *
 * @since MU (3.0.0)
 *
 * @param array $result
 * @return array
 */
function signup_nonce_check( $result ) {
	if ( ! strpos( $_SERVER['PHP_SELF'], 'wp-signup.php' ) ) {
		return $result;
	}

	if ( ! wp_verify_nonce( $_POST['_signup_form'], 'signup_form_' . $_POST['signup_form_id'] ) ) {
		$result['errors']->add( 'invalid_nonce', __( 'Unable to submit this form, please try again.' ) );
	}

	return $result;
}

/**
 * Corrects 404 redirects when NOBLOGREDIRECT is defined.
 *
 * @since MU (3.0.0)
 */
function maybe_redirect_404() {
	if ( is_main_site() && is_404() && defined( 'NOBLOGREDIRECT' ) ) {
		/**
		 * Filters the redirect URL for 404s on the main site.
		 *
		 * The filter is only evaluated if the NOBLOGREDIRECT constant is defined.
		 *
		 * @since 3.0.0
		 *
		 * @param string $no_blog_redirect The redirect URL defined in NOBLOGREDIRECT.
		 */
		$destination = apply_filters( 'blog_redirect_404', NOBLOGREDIRECT );

		if ( $destination ) {
			if ( '%siteurl%' === $destination ) {
				$destination = network_home_url();
			}

			wp_redirect( $destination );
			exit;
		}
	}
}

/**
 * Adds a new user to a blog by visiting /newbloguser/{key}/.
 *
 * This will only work when the user's details are saved as an option
 * keyed as 'new_user_{key}', where '{key}' is a hash generated for the user to be
 * added, as when a user is invited through the regular WP Add User interface.
 *
 * @since MU (3.0.0)
 */
function maybe_add_existing_user_to_blog() {
	if ( ! str_contains( $_SERVER['REQUEST_URI'], '/newbloguser/' ) ) {
		return;
	}

	$parts = explode( '/', $_SERVER['REQUEST_URI'] );
	$key   = array_pop( $parts );

	if ( '' === $key ) {
		$key = array_pop( $parts );
	}

	$details = get_option( 'new_user_' . $key );
	if ( ! empty( $details ) ) {
		delete_option( 'new_user_' . $key );
	}

	if ( empty( $details ) || is_wp_error( add_existing_user_to_blog( $details ) ) ) {
		wp_die(
			sprintf(
				/* translators: %s: Home URL. */
				__( 'An error occurred adding you to this site. Go to the <a href="%s">homepage</a>.' ),
				home_url()
			)
		);
	}

	wp_die(
		sprintf(
			/* translators: 1: Home URL, 2: Admin URL. */
			__( 'You have been added to this site. Please visit the <a href="%1$s">homepage</a> or <a href="%2$s">log in</a> using your username and password.' ),
			home_url(),
			admin_url()
		),
		__( 'WordPress &rsaquo; Success' ),
		array( 'response' => 200 )
	);
}

/**
 * Adds a user to a blog based on details from maybe_add_existing_user_to_blog().
 *
 * @since MU (3.0.0)
 *
 * @param array|false $details {
 *     User details. Must at least contain values for the keys listed below.
 *
 *     @type int    $user_id The ID of the user being added to the current blog.
 *     @type string $role    The role to be assigned to the user.
 * }
 * @return true|WP_Error|void True on success or a WP_Error object if the user doesn't exist
 *                            or could not be added. Void if $details array was not provided.
 */
function add_existing_user_to_blog( $details = false ) {
	if ( is_array( $details ) ) {
		$blog_id = get_current_blog_id();
		$result  = add_user_to_blog( $blog_id, $details['user_id'], $details['role'] );

		/**
		 * Fires immediately after an existing user is added to a site.
		 *
		 * @since MU (3.0.0)
		 *
		 * @param int           $user_id User ID.
		 * @param true|WP_Error $result  True on success or a WP_Error object if the user doesn't exist
		 *                               or could not be added.
		 */
		do_action( 'added_existing_user', $details['user_id'], $result );

		return $result;
	}
}

/**
 * Adds a newly created user to the appropriate blog
 *
 * To add a user in general, use add_user_to_blog(). This function
 * is specifically hooked into the {@see 'wpmu_activate_user'} action.
 *
 * @since MU (3.0.0)
 *
 * @see add_user_to_blog()
 *
 * @param int    $user_id  User ID.
 * @param string $password User password. Ignored.
 * @param array  $meta     Signup meta data.
 */
function add_new_user_to_blog(
	$user_id,
	#[\SensitiveParameter]
	$password,
	$meta
) {
	if ( ! empty( $meta['add_to_blog'] ) ) {
		$blog_id = $meta['add_to_blog'];
		$role    = $meta['new_role'];
		remove_user_from_blog( $user_id, get_network()->site_id ); // Remove user from main blog.

		$result = add_user_to_blog( $blog_id, $user_id, $role );

		if ( ! is_wp_error( $result ) ) {
			update_user_meta( $user_id, 'primary_blog', $blog_id );
		}
	}
}

/**
 * Corrects From host on outgoing mail to match the site domain.
 *
 * @since MU (3.0.0)
 *
 * @param PHPMailer\PHPMailer\PHPMailer $phpmailer The PHPMailer instance (passed by reference).
 */
function fix_phpmailer_messageid( $phpmailer ) {
	$phpmailer->Hostname = get_network()->domain;
}

/**
 * Determines whether a user is marked as a spammer, based on user login.
 *
 * @since MU (3.0.0)
 *
 * @param string|WP_User $user Optional. Defaults to current user. WP_User object,
 *                             or user login name as a string.
 * @return bool
 */
function is_user_spammy( $user = null ) {
	if ( ! ( $user instanceof WP_User ) ) {
		if ( $user ) {
			$user = get_user_by( 'login', $user );
		} else {
			$user = wp_get_current_user();
		}
	}

	return $user && isset( $user->spam ) && '1' === $user->spam;
}

/**
 * Updates this blog's 'public' setting in the global blogs table.
 *
 * Public blogs have a setting of 1, private blogs are 0.
 *
 * @since MU (3.0.0)
 *
 * @param int $old_value The old public value.
 * @param int $value     The new public value.
 */
function update_blog_public( $old_value, $value ) {
	update_blog_status( get_current_blog_id(), 'public', (int) $value );
}

/**
 * Determines whether users can self-register, based on Network settings.
 *
 * @since MU (3.0.0)
 *
 * @return bool
 */
function users_can_register_signup_filter() {
	$registration = get_site_option( 'registration' );
	return ( 'all' === $registration || 'user' === $registration );
}

/**
 * Ensures that the welcome message is not empty. Currently unused.
 *
 * @since MU (3.0.0)
 *
 * @param string $text
 * @return string
 */
function welcome_user_msg_filter( $text ) {
	if ( ! $text ) {
		remove_filter( 'site_option_welcome_user_email', 'welcome_user_msg_filter' );

		/* translators: Do not translate USERNAME, PASSWORD, LOGINLINK, SITE_NAME: those are placeholders. */
		$text = __(
			'Howdy USERNAME,

Your new account is set up.

You can log in with the following information:
Username: USERNAME
Password: PASSWORD
LOGINLINK

Thanks!

--The Team @ SITE_NAME'
		);
		update_site_option( 'welcome_user_email', $text );
	}
	return $text;
}

/**
 * Determines whether to force SSL on content.
 *
 * @since 2.8.5
 *
 * @param bool|null $force Optional. Whether to force SSL in admin screens. Default null.
 * @return bool True if forced, false if not forced.
 */
function force_ssl_content( $force = null ) {
	static $forced_content = false;

	if ( ! is_null( $force ) ) {
		$old_forced     = $forced_content;
		$forced_content = (bool) $force;
		return $old_forced;
	}

	return $forced_content;
}

/**
 * Formats a URL to use https.
 *
 * Useful as a filter.
 *
 * @since 2.8.5
 *
 * @param string $url URL.
 * @return string URL with https as the scheme.
 */
function filter_SSL( $url ) {  // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	if ( ! is_string( $url ) ) {
		return get_bloginfo( 'url' ); // Return home site URL with proper scheme.
	}

	if ( force_ssl_content() && is_ssl() ) {
		$url = set_url_scheme( $url, 'https' );
	}

	return $url;
}

/**
 * Schedules update of the network-wide counts for the current network.
 *
 * @since 3.1.0
 */
function wp_schedule_update_network_counts() {
	if ( ! is_main_site() ) {
		return;
	}

	if ( ! wp_next_scheduled( 'update_network_counts' ) && ! wp_installing() ) {
		wp_schedule_event( time(), 'twicedaily', 'update_network_counts' );
	}
}

/**
 * Updates the network-wide counts for the current network.
 *
 * @since 3.1.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 */
function wp_update_network_counts( $network_id = null ) {
	wp_update_network_user_counts( $network_id );
	wp_update_network_site_counts( $network_id );
}

/**
 * Updates the count of sites for the current network.
 *
 * If enabled through the {@see 'enable_live_network_counts'} filter, update the sites count
 * on a network when a site is created or its status is updated.
 *
 * @since 3.7.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 */
function wp_maybe_update_network_site_counts( $network_id = null ) {
	$is_small_network = ! wp_is_large_network( 'sites', $network_id );

	/**
	 * Filters whether to update network site or user counts when a new site is created.
	 *
	 * @since 3.7.0
	 *
	 * @see wp_is_large_network()
	 *
	 * @param bool   $small_network Whether the network is considered small.
	 * @param string $context       Context. Either 'users' or 'sites'.
	 */
	if ( ! apply_filters( 'enable_live_network_counts', $is_small_network, 'sites' ) ) {
		return;
	}

	wp_update_network_site_counts( $network_id );
}

/**
 * Updates the network-wide users count.
 *
 * If enabled through the {@see 'enable_live_network_counts'} filter, update the users count
 * on a network when a user is created or its status is updated.
 *
 * @since 3.7.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 */
function wp_maybe_update_network_user_counts( $network_id = null ) {
	$is_small_network = ! wp_is_large_network( 'users', $network_id );

	/** This filter is documented in wp-includes/ms-functions.php */
	if ( ! apply_filters( 'enable_live_network_counts', $is_small_network, 'users' ) ) {
		return;
	}

	wp_update_network_user_counts( $network_id );
}

/**
 * Updates the network-wide site count.
 *
 * @since 3.7.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 */
function wp_update_network_site_counts( $network_id = null ) {
	$network_id = (int) $network_id;
	if ( ! $network_id ) {
		$network_id = get_current_network_id();
	}

	$count = get_sites(
		array(
			'network_id'             => $network_id,
			'spam'                   => 0,
			'deleted'                => 0,
			'archived'               => 0,
			'count'                  => true,
			'update_site_meta_cache' => false,
		)
	);

	update_network_option( $network_id, 'blog_count', $count );
}

/**
 * Updates the network-wide user count.
 *
 * @since 3.7.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 * @since 6.0.0 This function is now a wrapper for wp_update_user_counts().
 *
 * @param int|null $network_id ID of the network. Default is the current network.
 */
function wp_update_network_user_counts( $network_id = null ) {
	wp_update_user_counts( $network_id );
}

/**
 * Returns the space used by the current site.
 *
 * @since 3.5.0
 *
 * @return int Used space in megabytes.
 */
function get_space_used() {
	/**
	 * Filters the amount of storage space used by the current site, in megabytes.
	 *
	 * @since 3.5.0
	 *
	 * @param int|false $space_used The amount of used space, in megabytes. Default false.
	 */
	$space_used = apply_filters( 'pre_get_space_used', false );

	if ( false === $space_used ) {
		$upload_dir = wp_upload_dir();
		$space_used = get_dirsize( $upload_dir['basedir'] ) / MB_IN_BYTES;
	}

	return $space_used;
}

/**
 * Returns the upload quota for the current blog.
 *
 * @since MU (3.0.0)
 *
 * @return int Quota in megabytes.
 */
function get_space_allowed() {
	$space_allowed = get_option( 'blog_upload_space' );

	if ( ! is_numeric( $space_allowed ) ) {
		$space_allowed = get_site_option( 'blog_upload_space' );
	}

	if ( ! is_numeric( $space_allowed ) ) {
		$space_allowed = 100;
	}

	/**
	 * Filters the upload quota for the current site.
	 *
	 * @since 3.7.0
	 *
	 * @param int $space_allowed Upload quota in megabytes for the current blog.
	 */
	return apply_filters( 'get_space_allowed', $space_allowed );
}

/**
 * Determines if there is any upload space left in the current blog's quota.
 *
 * @since 3.0.0
 *
 * @return int of upload space available in bytes.
 */
function get_upload_space_available() {
	$allowed = get_space_allowed();
	if ( $allowed < 0 ) {
		$allowed = 0;
	}
	$space_allowed = $allowed * MB_IN_BYTES;
	if ( get_site_option( 'upload_space_check_disabled' ) ) {
		return $space_allowed;
	}

	$space_used = get_space_used() * MB_IN_BYTES;

	if ( ( $space_allowed - $space_used ) <= 0 ) {
		return 0;
	}

	return $space_allowed - $space_used;
}

/**
 * Determines if there is any upload space left in the current blog's quota.
 *
 * @since 3.0.0
 * @return bool True if space is available, false otherwise.
 */
function is_upload_space_available() {
	if ( get_site_option( 'upload_space_check_disabled' ) ) {
		return true;
	}

	return (bool) get_upload_space_available();
}

/**
 * Filters the maximum upload file size allowed, in bytes.
 *
 * @since 3.0.0
 *
 * @param int $size Upload size limit in bytes.
 * @return int Upload size limit in bytes.
 */
function upload_size_limit_filter( $size ) {
	$fileupload_maxk         = (int) get_site_option( 'fileupload_maxk', 1500 );
	$max_fileupload_in_bytes = KB_IN_BYTES * $fileupload_maxk;

	if ( get_site_option( 'upload_space_check_disabled' ) ) {
		return min( $size, $max_fileupload_in_bytes );
	}

	return min( $size, $max_fileupload_in_bytes, get_upload_space_available() );
}

/**
 * Determines whether or not we have a large network.
 *
 * The default criteria for a large network is either more than 10,000 users or more than 10,000 sites.
 * Plugins can alter this criteria using the {@see 'wp_is_large_network'} filter.
 *
 * @since 3.3.0
 * @since 4.8.0 The `$network_id` parameter has been added.
 *
 * @param string   $using      'sites' or 'users'. Default is 'sites'.
 * @param int|null $network_id ID of the network. Default is the current network.
 * @return bool True if the network meets the criteria for large. False otherwise.
 */
function wp_is_large_network( $using = 'sites', $network_id = null ) {
	$network_id = (int) $network_id;
	if ( ! $network_id ) {
		$network_id = get_current_network_id();
	}

	if ( 'users' === $using ) {
		$count = get_user_count( $network_id );

		$is_large_network = wp_is_large_user_count( $network_id );

		/**
		 * Filters whether the network is considered large.
		 *
		 * @since 3.3.0
		 * @since 4.8.0 The `$network_id` parameter has been added.
		 *
		 * @param bool   $is_large_network Whether the network has more than 10000 users or sites.
		 * @param string $component        The component to count. Accepts 'users', or 'sites'.
		 * @param int    $count            The count of items for the component.
		 * @param int    $network_id       The ID of the network being checked.
		 */
		return apply_filters( 'wp_is_large_network', $is_large_network, 'users', $count, $network_id );
	}

	$count = get_blog_count( $network_id );

	/** This filter is documented in wp-includes/ms-functions.php */
	return apply_filters( 'wp_is_large_network', $count > 10000, 'sites', $count, $network_id );
}

/**
 * Retrieves a list of reserved site on a sub-directory Multisite installation.
 *
 * @since 4.4.0
 *
 * @return string[] Array of reserved names.
 */
function get_subdirectory_reserved_names() {
	$names = array(
		'page',
		'comments',
		'blog',
		'files',
		'feed',
		'wp-admin',
		'wp-content',
		'wp-includes',
		'wp-json',
		'embed',
	);

	/**
	 * Filters reserved site names on a sub-directory Multisite installation.
	 *
	 * @since 3.0.0
	 * @since 4.4.0 'wp-admin', 'wp-content', 'wp-includes', 'wp-json', and 'embed' were added
	 *              to the reserved names list.
	 *
	 * @param string[] $subdirectory_reserved_names Array of reserved names.
	 */
	return apply_filters( 'subdirectory_reserved_names', $names );
}

/**
 * Sends a confirmation request email when a change of network admin email address is attempted.
 *
 * The new network admin address will not become active until confirmed.
 *
 * @since 4.9.0
 *
 * @param string $old_value The old network admin email address.
 * @param string $value     The proposed new network admin email address.
 */
function update_network_option_new_admin_email( $old_value, $value ) {
	if ( get_site_option( 'admin_email' ) === $value || ! is_email( $value ) ) {
		return;
	}

	$hash            = md5( $value . time() . mt_rand() );
	$new_admin_email = array(
		'hash'     => $hash,
		'newemail' => $value,
	);
	update_site_option( 'network_admin_hash', $new_admin_email );

	$switched_locale = switch_to_user_locale( get_current_user_id() );

	/* translators: Do not translate USERNAME, ADMIN_URL, EMAIL, SITENAME, SITEURL: those are placeholders. */
	$email_text = __(
		'Howdy ###USERNAME###,

You recently requested to have the network admin email address on
your network changed.

If this is correct, please click on the following link to change it:
###ADMIN_URL###

You can safely ignore and delete this email if you do not want to
take this action.

This email has been sent to ###EMAIL###

Regards,
All at ###SITENAME###
###SITEURL###'
	);

	/**
	 * Filters the text of the email sent when a change of network admin email address is attempted.
	 *
	 * The following strings have a special meaning and will get replaced dynamically:
	 *
	 *  - `###USERNAME###`  The current user's username.
	 *  - `###ADMIN_URL###` The link to click on to confirm the email change.
	 *  - `###EMAIL###`     The proposed new network admin email address.
	 *  - `###SITENAME###`  The name of the network.
	 *  - `###SITEURL###`   The URL to the network.
	 *
	 * @since 4.9.0
	 *
	 * @param string $email_text      Text in the email.
	 * @param array  $new_admin_email {
	 *     Data relating to the new network admin email address.
	 *
	 *     @type string $hash     The secure hash used in the confirmation link URL.
	 *     @type string $newemail The proposed new network admin email address.
	 * }
	 */
	$content = apply_filters( 'new_network_admin_email_content', $email_text, $new_admin_email );

	$current_user = wp_get_current_user();
	$content      = str_replace( '###USERNAME###', $current_user->user_login, $content );
	$content      = str_replace( '###ADMIN_URL###', esc_url( network_admin_url( 'settings.php?network_admin_hash=' . $hash ) ), $content );
	$content      = str_replace( '###EMAIL###', $value, $content );
	$content      = str_replace( '###SITENAME###', wp_specialchars_decode( get_site_option( 'site_name' ), ENT_QUOTES ), $content );
	$content      = str_replace( '###SITEURL###', network_home_url(), $content );

	wp_mail(
		$value,
		sprintf(
			/* translators: Email change notification email subject. %s: Network title. */
			__( '[%s] Network Admin Email Change Request' ),
			wp_specialchars_decode( get_site_option( 'site_name' ), ENT_QUOTES )
		),
		$content
	);

	if ( $switched_locale ) {
		restore_previous_locale();
	}
}

/**
 * Sends an email to the old network admin email address when the network admin email address changes.
 *
 * @since 4.9.0
 *
 * @param string $option_name The relevant database option name.
 * @param string $new_email   The new network admin email address.
 * @param string $old_email   The old network admin email address.
 * @param int    $network_id  ID of the network.
 */
function wp_network_admin_email_change_notification( $option_name, $new_email, $old_email, $network_id ) {
	$send = true;

	// Don't send the notification for an empty email address or the default 'admin_email' value.
	if ( empty( $old_email ) || 'you@example.com' === $old_email ) {
		$send = false;
	}

	/**
	 * Filters whether to send the network admin email change notification email.
	 *
	 * @since 4.9.0
	 *
	 * @param bool   $send       Whether to send the email notification.
	 * @param string $old_email  The old network admin email address.
	 * @param string $new_email  The new network admin email address.
	 * @param int    $network_id ID of the network.
	 */
	$send = apply_filters( 'send_network_admin_email_change_email', $send, $old_email, $new_email, $network_id );

	if ( ! $send ) {
		return;
	}

	/* translators: Do not translate OLD_EMAIL, NEW_EMAIL, SITENAME, SITEURL: those are placeholders. */
	$email_change_text = __(
		'Hi,

This notice confirms that the network admin email address was changed on ###SITENAME###.

The new network admin email address is ###NEW_EMAIL###.

This email has been sent to ###OLD_EMAIL###

Regards,
All at ###SITENAME###
###SITEURL###'
	);

	$email_change_email = array(
		'to'      => $old_email,
		/* translators: Network admin email change notification email subject. %s: Network title. */
		'subject' => __( '[%s] Network Admin Email Changed' ),
		'message' => $email_change_text,
		'headers' => '',
	);
	// Get network name.
	$network_name = wp_specialchars_decode( get_site_option( 'site_name' ), ENT_QUOTES );

	/**
	 * Filters the contents of the email notification sent when the network admin email address is changed.
	 *
	 * @since 4.9.0
	 *
	 * @param array $email_change_email {
	 *     Used to build wp_mail().
	 *
	 *     @type string $to      The intended recipient.
	 *     @type string $subject The subject of the email.
	 *     @type string $message The content of the email.
	 *         The following strings have a special meaning and will get replaced dynamically:
	 *          - `###OLD_EMAIL###` The old network admin email address.
	 *          - `###NEW_EMAIL###` The new network admin email address.
	 *          - `###SITENAME###`  The name of the network.
	 *          - `###SITEURL###`   The URL to the site.
	 *     @type string $headers Headers.
	 * }
	 * @param string $old_email  The old network admin email address.
	 * @param string $new_email  The new network admin email address.
	 * @param int    $network_id ID of the network.
	 */
	$email_change_email = apply_filters( 'network_admin_email_change_email', $email_change_email, $old_email, $new_email, $network_id );

	$email_change_email['message'] = str_replace( '###OLD_EMAIL###', $old_email, $email_change_email['message'] );
	$email_change_email['message'] = str_replace( '###NEW_EMAIL###', $new_email, $email_change_email['message'] );
	$email_change_email['message'] = str_replace( '###SITENAME###', $network_name, $email_change_email['message'] );
	$email_change_email['message'] = str_replace( '###SITEURL###', home_url(), $email_change_email['message'] );

	wp_mail(
		$email_change_email['to'],
		sprintf(
			$email_change_email['subject'],
			$network_name
		),
		$email_change_email['message'],
		$email_change_email['headers']
	);
}
                                                                                                                                                                                                                                                                                                                              ./wp-includes/wp-includes.php                                                                       0000644 0001750 0001750 00000000000 15152744201 016655  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               ./wp-includes/class-wp-theme-json.php                                                               0000644 0001750 0001750 00000500773 15152744177 020265  0                                                                                                    ustar   bvserver                        bvserver                                                                                                                                                                                                               <?php
/**
 * WP_Theme_JSON class
 *
 * @package WordPress
 * @subpackage Theme
 * @since 5.8.0
 */

/**
 * Class that encapsulates the processing of structures that adhere to the theme.json spec.
 *
 * This class is for internal core usage and is not supposed to be used by extenders (plugins and/or themes).
 * This is a low-level API that may need to do breaking changes. Please,
 * use get_global_settings, get_global_styles, and get_global_stylesheet instead.
 *
 * @access private
 */
#[AllowDynamicProperties]
class WP_Theme_JSON {

	/**
	 * Container of data in theme.json format.
	 *
	 * @since 5.8.0
	 * @var array
	 */
	protected $theme_json = null;

	/**
	 * Holds block metadata extracted from block.json
	 * to be shared among all instances so we don't
	 * process it twice.
	 *
	 * @since 5.8.0
	 * @since 6.1.0 Initialize as an empty array.
	 * @var array
	 */
	protected static $blocks_metadata = array();

	/**
	 * The CSS selector for the top-level preset settings.
	 *
	 * @since 6.6.0
	 * @var string
	 */
	const ROOT_CSS_PROPERTIES_SELECTOR = ':root';

	/**
	 * The CSS selector for the top-level styles.
	 *
	 * @since 5.8.0
	 * @var string
	 */
	const ROOT_BLOCK_SELECTOR = 'body';

	/**
	 * The sources of data this object can represent.
	 *
	 * @since 5.8.0
	 * @since 6.1.0 Added 'blocks'.
	 * @var string[]
	 */
	const VALID_ORIGINS = array(
		'default',
		'blocks',
		'theme',
		'custom',
	);

	/**
	 * Presets are a set of values that serve
	 * to bootstrap some styles: colors, font sizes, etc.
	 *
	 * They are a unkeyed array of values such as:
	 *
	 *     array(
	 *       array(
	 *         'slug'      => 'unique-name-within-the-set',
	 *         'name'      => 'Name for the UI',
	 *         <value_key> => 'value'
	 *       ),
	 *     )
	 *
	 * This contains the necessary metadata to process them:
	 *
	 * - path             => Where to find the preset within the settings section.
	 * - prevent_override => Disables override of default presets by theme presets.
	 *                       The relationship between whether to override the defaults
	 *                       and whether the defaults are enabled is inverse:
	 *                         - If defaults are enabled  => theme presets should not be overridden
	 *                         - If defaults are disabled => theme presets should be overridden
	 *                       For example, a theme sets defaultPalette to false,
	 *                       making the default palette hidden from the user.
	 *                       In that case, we want all the theme presets to be present,
	 *                       so they should override the defaults by setting this false.
	 * - use_default_names => whether to use the default names
	 * - value_key        => the key that represents the value
	 * - value_func       => optionally, instead of value_key, a function to generate
	 *                       the value that takes a preset as an argument
	 *                       (either value_key or value_func should be present)
	 * - css_vars         => template string to use in generating the CSS Custom Property.
	 *                       Example output: "--wp--preset--duotone--blue: <value>" will generate as many CSS Custom Properties as presets defined
	 *                       substituting the $slug for the slug's value for each preset value.
	 * - classes          => array containing a structure with the classes to
	 *                       generate for the presets, where for each array item
	 *                       the key is the class name and the value the property name.
	 *                       The "$slug" substring will be replaced by the slug of each preset.
	 *                       For example:
	 *                       'classes' => array(
	 *                         '.has-$slug-color'            => 'color',
	 *                         '.has-$slug-background-color' => 'background-color',
	 *                         '.has-$slug-border-color'     => 'border-color',
	 *                       )
	 * - properties       => array of CSS properties to be used by kses to
	 *                       validate the content of each preset
	 *                       by means of the remove_insecure_properties method.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `color.duotone` and `typography.fontFamilies` presets,
	 *              `use_default_names` preset key, and simplified the metadata structure.
	 * @since 6.0.0 Replaced `override` with `prevent_override` and updated the
	 *              `prevent_override` value for `color.duotone` to use `color.defaultDuotone`.
	 * @since 6.2.0 Added 'shadow' presets.
	 * @since 6.3.0 Replaced value_func for duotone with `null`. Custom properties are handled by class-wp-duotone.php.
	 * @since 6.6.0 Added the `dimensions.aspectRatios` and `dimensions.defaultAspectRatios` presets.
	 *              Updated the 'prevent_override' value for font size presets to use 'typography.defaultFontSizes'
	 *              and spacing size presets to use `spacing.defaultSpacingSizes`.
	 * @since 6.9.0 Added `border.radiusSizes`.
	 * @var array
	 */
	const PRESETS_METADATA = array(
		array(
			'path'              => array( 'dimensions', 'aspectRatios' ),
			'prevent_override'  => array( 'dimensions', 'defaultAspectRatios' ),
			'use_default_names' => false,
			'value_key'         => 'ratio',
			'css_vars'          => '--wp--preset--aspect-ratio--$slug',
			'classes'           => array(),
			'properties'        => array( 'aspect-ratio' ),
		),
		array(
			'path'              => array( 'color', 'palette' ),
			'prevent_override'  => array( 'color', 'defaultPalette' ),
			'use_default_names' => false,
			'value_key'         => 'color',
			'css_vars'          => '--wp--preset--color--$slug',
			'classes'           => array(
				'.has-$slug-color'            => 'color',
				'.has-$slug-background-color' => 'background-color',
				'.has-$slug-border-color'     => 'border-color',
			),
			'properties'        => array( 'color', 'background-color', 'border-color' ),
		),
		array(
			'path'              => array( 'color', 'gradients' ),
			'prevent_override'  => array( 'color', 'defaultGradients' ),
			'use_default_names' => false,
			'value_key'         => 'gradient',
			'css_vars'          => '--wp--preset--gradient--$slug',
			'classes'           => array( '.has-$slug-gradient-background' => 'background' ),
			'properties'        => array( 'background' ),
		),
		array(
			'path'              => array( 'color', 'duotone' ),
			'prevent_override'  => array( 'color', 'defaultDuotone' ),
			'use_default_names' => false,
			'value_func'        => null, // CSS Custom Properties for duotone are handled by block supports in class-wp-duotone.php.
			'css_vars'          => null,
			'classes'           => array(),
			'properties'        => array( 'filter' ),
		),
		array(
			'path'              => array( 'typography', 'fontSizes' ),
			'prevent_override'  => array( 'typography', 'defaultFontSizes' ),
			'use_default_names' => true,
			'value_func'        => 'wp_get_typography_font_size_value',
			'css_vars'          => '--wp--preset--font-size--$slug',
			'classes'           => array( '.has-$slug-font-size' => 'font-size' ),
			'properties'        => array( 'font-size' ),
		),
		array(
			'path'              => array( 'typography', 'fontFamilies' ),
			'prevent_override'  => false,
			'use_default_names' => false,
			'value_key'         => 'fontFamily',
			'css_vars'          => '--wp--preset--font-family--$slug',
			'classes'           => array( '.has-$slug-font-family' => 'font-family' ),
			'properties'        => array( 'font-family' ),
		),
		array(
			'path'              => array( 'spacing', 'spacingSizes' ),
			'prevent_override'  => array( 'spacing', 'defaultSpacingSizes' ),
			'use_default_names' => true,
			'value_key'         => 'size',
			'css_vars'          => '--wp--preset--spacing--$slug',
			'classes'           => array(),
			'properties'        => array( 'padding', 'margin' ),
		),
		array(
			'path'              => array( 'shadow', 'presets' ),
			'prevent_override'  => array( 'shadow', 'defaultPresets' ),
			'use_default_names' => false,
			'value_key'         => 'shadow',
			'css_vars'          => '--wp--preset--shadow--$slug',
			'classes'           => array(),
			'properties'        => array( 'box-shadow' ),
		),
		array(
			'path'              => array( 'border', 'radiusSizes' ),
			'prevent_override'  => false,
			'use_default_names' => false,
			'value_key'         => 'size',
			'css_vars'          => '--wp--preset--border-radius--$slug',
			'classes'           => array(),
			'properties'        => array( 'border-radius' ),
		),
	);

	/**
	 * Metadata for style properties.
	 *
	 * Each element is a direct mapping from the CSS property name to the
	 * path to the value in theme.json & block attributes.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `border-*`, `font-family`, `font-style`, `font-weight`,
	 *              `letter-spacing`, `margin-*`, `padding-*`, `--wp--style--block-gap`,
	 *              `text-decoration`, `text-transform`, and `filter` properties,
	 *              simplified the metadata structure.
	 * @since 6.1.0 Added the `border-*-color`, `border-*-width`, `border-*-style`,
	 *              `--wp--style--root--padding-*`, and `box-shadow` properties,
	 *              removed the `--wp--style--block-gap` property.
	 * @since 6.2.0 Added `outline-*`, and `min-height` properties.
	 * @since 6.3.0 Added `column-count` property.
	 * @since 6.4.0 Added `writing-mode` property.
	 * @since 6.5.0 Added `aspect-ratio` property.
	 * @since 6.6.0 Added `background-[image|position|repeat|size]` properties.
	 * @since 6.7.0 Added `background-attachment` property.
	 * @var array
	 */
	const PROPERTIES_METADATA = array(
		'aspect-ratio'                      => array( 'dimensions', 'aspectRatio' ),
		'background'                        => array( 'color', 'gradient' ),
		'background-color'                  => array( 'color', 'background' ),
		'background-image'                  => array( 'background', 'backgroundImage' ),
		'background-position'               => array( 'background', 'backgroundPosition' ),
		'background-repeat'                 => array( 'background', 'backgroundRepeat' ),
		'background-size'                   => array( 'background', 'backgroundSize' ),
		'background-attachment'             => array( 'background', 'backgroundAttachment' ),
		'border-radius'                     => array( 'border', 'radius' ),
		'border-top-left-radius'            => array( 'border', 'radius', 'topLeft' ),
		'border-top-right-radius'           => array( 'border', 'radius', 'topRight' ),
		'border-bottom-left-radius'         => array( 'border', 'radius', 'bottomLeft' ),
		'border-bottom-right-radius'        => array( 'border', 'radius', 'bottomRight' ),
		'border-color'                      => array( 'border', 'color' ),
		'border-width'                      => array( 'border', 'width' ),
		'border-style'                      => array( 'border', 'style' ),
		'border-top-color'                  => array( 'border', 'top', 'color' ),
		'border-top-width'                  => array( 'border', 'top', 'width' ),
		'border-top-style'                  => array( 'border', 'top', 'style' ),
		'border-right-color'                => array( 'border', 'right', 'color' ),
		'border-right-width'                => array( 'border', 'right', 'width' ),
		'border-right-style'                => array( 'border', 'right', 'style' ),
		'border-bottom-color'               => array( 'border', 'bottom', 'color' ),
		'border-bottom-width'               => array( 'border', 'bottom', 'width' ),
		'border-bottom-style'               => array( 'border', 'bottom', 'style' ),
		'border-left-color'                 => array( 'border', 'left', 'color' ),
		'border-left-width'                 => array( 'border', 'left', 'width' ),
		'border-left-style'                 => array( 'border', 'left', 'style' ),
		'color'                             => array( 'color', 'text' ),
		'text-align'                        => array( 'typography', 'textAlign' ),
		'column-count'                      => array( 'typography', 'textColumns' ),
		'font-family'                       => array( 'typography', 'fontFamily' ),
		'font-size'                         => array( 'typography', 'fontSize' ),
		'font-style'                        => array( 'typography', 'fontStyle' ),
		'font-weight'                       => array( 'typography', 'fontWeight' ),
		'letter-spacing'                    => array( 'typography', 'letterSpacing' ),
		'line-height'                       => array( 'typography', 'lineHeight' ),
		'margin'                            => array( 'spacing', 'margin' ),
		'margin-top'                        => array( 'spacing', 'margin', 'top' ),
		'margin-right'                      => array( 'spacing', 'margin', 'right' ),
		'margin-bottom'                     => array( 'spacing', 'margin', 'bottom' ),
		'margin-left'                       => array( 'spacing', 'margin', 'left' ),
		'min-height'                        => array( 'dimensions', 'minHeight' ),
		'outline-color'                     => array( 'outline', 'color' ),
		'outline-offset'                    => array( 'outline', 'offset' ),
		'outline-style'                     => array( 'outline', 'style' ),
		'outline-width'                     => array( 'outline', 'width' ),
		'padding'                           => array( 'spacing', 'padding' ),
		'padding-top'                       => array( 'spacing', 'padding', 'top' ),
		'padding-right'                     => array( 'spacing', 'padding', 'right' ),
		'padding-bottom'                    => array( 'spacing', 'padding', 'bottom' ),
		'padding-left'                      => array( 'spacing', 'padding', 'left' ),
		'--wp--style--root--padding'        => array( 'spacing', 'padding' ),
		'--wp--style--root--padding-top'    => array( 'spacing', 'padding', 'top' ),
		'--wp--style--root--padding-right'  => array( 'spacing', 'padding', 'right' ),
		'--wp--style--root--padding-bottom' => array( 'spacing', 'padding', 'bottom' ),
		'--wp--style--root--padding-left'   => array( 'spacing', 'padding', 'left' ),
		'text-decoration'                   => array( 'typography', 'textDecoration' ),
		'text-transform'                    => array( 'typography', 'textTransform' ),
		'filter'                            => array( 'filter', 'duotone' ),
		'box-shadow'                        => array( 'shadow' ),
		'writing-mode'                      => array( 'typography', 'writingMode' ),
	);

	/**
	 * Indirect metadata for style properties that are not directly output.
	 *
	 * Each element maps from a CSS property name to an array of
	 * paths to the value in theme.json & block attributes.
	 *
	 * Indirect properties are not output directly by `compute_style_properties`,
	 * but are used elsewhere in the processing of global styles. The indirect
	 * property is used to validate whether a style value is allowed.
	 *
	 * @since 6.2.0
	 * @since 6.6.0 Added background-image properties.
	 * @var array
	 */
	const INDIRECT_PROPERTIES_METADATA = array(
		'gap'              => array(
			array( 'spacing', 'blockGap' ),
		),
		'column-gap'       => array(
			array( 'spacing', 'blockGap', 'left' ),
		),
		'row-gap'          => array(
			array( 'spacing', 'blockGap', 'top' ),
		),
		'max-width'        => array(
			array( 'layout', 'contentSize' ),
			array( 'layout', 'wideSize' ),
		),
		'background-image' => array(
			array( 'background', 'backgroundImage', 'url' ),
		),
	);

	/**
	 * Protected style properties.
	 *
	 * These style properties are only rendered if a setting enables it
	 * via a value other than `null`.
	 *
	 * Each element maps the style property to the corresponding theme.json
	 * setting key.
	 *
	 * @since 5.9.0
	 * @var array
	 */
	const PROTECTED_PROPERTIES = array(
		'spacing.blockGap' => array( 'spacing', 'blockGap' ),
	);

	/**
	 * The top-level keys a theme.json can have.
	 *
	 * @since 5.8.0 As `ALLOWED_TOP_LEVEL_KEYS`.
	 * @since 5.9.0 Renamed from `ALLOWED_TOP_LEVEL_KEYS` to `VALID_TOP_LEVEL_KEYS`,
	 *              added the `customTemplates` and `templateParts` values.
	 * @since 6.3.0 Added the `description` value.
	 * @since 6.6.0 Added `blockTypes` to support block style variation theme.json partials.
	 * @var string[]
	 */
	const VALID_TOP_LEVEL_KEYS = array(
		'blockTypes',
		'customTemplates',
		'description',
		'patterns',
		'settings',
		'slug',
		'styles',
		'templateParts',
		'title',
		'version',
	);

	/**
	 * The valid properties under the settings key.
	 *
	 * @since 5.8.0 As `ALLOWED_SETTINGS`.
	 * @since 5.9.0 Renamed from `ALLOWED_SETTINGS` to `VALID_SETTINGS`,
	 *              added new properties for `border`, `color`, `spacing`,
	 *              and `typography`, and renamed others according to the new schema.
	 * @since 6.0.0 Added `color.defaultDuotone`.
	 * @since 6.1.0 Added `layout.definitions` and `useRootPaddingAwareAlignments`.
	 * @since 6.2.0 Added `dimensions.minHeight`, 'shadow.presets', 'shadow.defaultPresets',
	 *              `position.fixed` and `position.sticky`.
	 * @since 6.3.0 Added support for `typography.textColumns`, removed `layout.definitions`.
	 * @since 6.4.0 Added support for `layout.allowEditing`, `background.backgroundImage`,
	 *              `typography.writingMode`, `lightbox.enabled` and `lightbox.allowEditing`.
	 * @since 6.5.0 Added support for `layout.allowCustomContentAndWideSize`,
	 *              `background.backgroundSize` and `dimensions.aspectRatio`.
	 * @since 6.6.0 Added support for 'dimensions.aspectRatios', 'dimensions.defaultAspectRatios',
	 *              'typography.defaultFontSizes', and 'spacing.defaultSpacingSizes'.
	 * @since 6.9.0 Added support for `border.radiusSizes`.
	 * @var array
	 */
	const VALID_SETTINGS = array(
		'appearanceTools'               => null,
		'useRootPaddingAwareAlignments' => null,
		'background'                    => array(
			'backgroundImage' => null,
			'backgroundSize'  => null,
		),
		'border'                        => array(
			'color'       => null,
			'radius'      => null,
			'radiusSizes' => null,
			'style'       => null,
			'width'       => null,
		),
		'color'                         => array(
			'background'       => null,
			'custom'           => null,
			'customDuotone'    => null,
			'customGradient'   => null,
			'defaultDuotone'   => null,
			'defaultGradients' => null,
			'defaultPalette'   => null,
			'duotone'          => null,
			'gradients'        => null,
			'link'             => null,
			'heading'          => null,
			'button'           => null,
			'caption'          => null,
			'palette'          => null,
			'text'             => null,
		),
		'custom'                        => null,
		'dimensions'                    => array(
			'aspectRatio'         => null,
			'aspectRatios'        => null,
			'defaultAspectRatios' => null,
			'minHeight'           => null,
		),
		'layout'                        => array(
			'contentSize'                   => null,
			'wideSize'                      => null,
			'allowEditing'                  => null,
			'allowCustomContentAndWideSize' => null,
		),
		'lightbox'                      => array(
			'enabled'      => null,
			'allowEditing' => null,
		),
		'position'                      => array(
			'fixed'  => null,
			'sticky' => null,
		),
		'spacing'                       => array(
			'customSpacingSize'   => null,
			'defaultSpacingSizes' => null,
			'spacingSizes'        => null,
			'spacingScale'        => null,
			'blockGap'            => null,
			'margin'              => null,
			'padding'             => null,
			'units'               => null,
		),
		'shadow'                        => array(
			'presets'        => null,
			'defaultPresets' => null,
		),
		'typography'                    => array(
			'fluid'            => null,
			'customFontSize'   => null,
			'defaultFontSizes' => null,
			'dropCap'          => null,
			'fontFamilies'     => null,
			'fontSizes'        => null,
			'fontStyle'        => null,
			'fontWeight'       => null,
			'letterSpacing'    => null,
			'lineHeight'       => null,
			'textAlign'        => null,
			'textColumns'      => null,
			'textDecoration'   => null,
			'textTransform'    => null,
			'writingMode'      => null,
		),
	);

	/**
	 * The valid properties for fontFamilies under settings key.
	 *
	 * @since 6.5.0
	 * @var array
	 */
	const FONT_FAMILY_SCHEMA = array(
		array(
			'fontFamily' => null,
			'name'       => null,
			'slug'       => null,
			'fontFace'   => array(
				array(
					'ascentOverride'        => null,
					'descentOverride'       => null,
					'fontDisplay'           => null,
					'fontFamily'            => null,
					'fontFeatureSettings'   => null,
					'fontStyle'             => null,
					'fontStretch'           => null,
					'fontVariationSettings' => null,
					'fontWeight'            => null,
					'lineGapOverride'       => null,
					'sizeAdjust'            => null,
					'src'                   => null,
					'unicodeRange'          => null,
				),
			),
		),
	);

	/**
	 * The valid properties under the styles key.
	 *
	 * @since 5.8.0 As `ALLOWED_STYLES`.
	 * @since 5.9.0 Renamed from `ALLOWED_STYLES` to `VALID_STYLES`,
	 *              added new properties for `border`, `filter`, `spacing`,
	 *              and `typography`.
	 * @since 6.1.0 Added new side properties for `border`,
	 *              added new property `shadow`,
	 *              updated `blockGap` to be allowed at any level.
	 * @since 6.2.0 Added `outline`, and `minHeight` properties.
	 * @since 6.3.0 Added support for `typography.textColumns`.
	 * @since 6.5.0 Added support for `dimensions.aspectRatio`.
	 * @since 6.6.0 Added `background` sub properties to top-level only.
	 * @var array
	 */
	const VALID_STYLES = array(
		'background' => array(
			'backgroundImage'      => null,
			'backgroundPosition'   => null,
			'backgroundRepeat'     => null,
			'backgroundSize'       => null,
			'backgroundAttachment' => null,
		),
		'border'     => array(
			'color'  => null,
			'radius' => null,
			'style'  => null,
			'width'  => null,
			'top'    => null,
			'right'  => null,
			'bottom' => null,
			'left'   => null,
		),
		'color'      => array(
			'background' => null,
			'gradient'   => null,
			'text'       => null,
		),
		'dimensions' => array(
			'aspectRatio' => null,
			'minHeight'   => null,
		),
		'filter'     => array(
			'duotone' => null,
		),
		'outline'    => array(
			'color'  => null,
			'offset' => null,
			'style'  => null,
			'width'  => null,
		),
		'shadow'     => null,
		'spacing'    => array(
			'margin'   => null,
			'padding'  => null,
			'blockGap' => null,
		),
		'typography' => array(
			'fontFamily'     => null,
			'fontSize'       => null,
			'fontStyle'      => null,
			'fontWeight'     => null,
			'letterSpacing'  => null,
			'lineHeight'     => null,
			'textAlign'      => null,
			'textColumns'    => null,
			'textDecoration' => null,
			'textTransform'  => null,
			'writingMode'    => null,
		),
		'css'        => null,
	);

	/**
	 * Defines which pseudo selectors are enabled for which elements.
	 *
	 * The order of the selectors should be: link, any-link, visited, hover, focus, focus-visible, active.
	 * This is to ensure the user action (hover, focus and active) styles have a higher
	 * specificity than the visited styles, which in turn have a higher specificity than
	 * the unvisited styles.
	 *
	 * See https://core.trac.wordpress.org/ticket/56928.
	 * Note: this will affect both top-level and block-level elements.
	 *
	 * @since 6.1.0
	 * @since 6.2.0 Added support for ':link' and ':any-link'.
	 * @since 6.8.0 Added support for ':focus-visible'.
	 * @since 6.9.0 Added `textInput` and `select` elements.
	 * @var array
	 */
	const VALID_ELEMENT_PSEUDO_SELECTORS = array(
		'link'   => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
		'button' => array( ':link', ':any-link', ':visited', ':hover', ':focus', ':focus-visible', ':active' ),
	);

	/**
	 * The valid elements that can be found under styles.
	 *
	 * @since 5.8.0
	 * @since 6.1.0 Added `heading`, `button`, and `caption` elements.
	 * @var string[]
	 */
	const ELEMENTS = array(
		'link'      => 'a:where(:not(.wp-element-button))', // The `where` is needed to lower the specificity.
		'heading'   => 'h1, h2, h3, h4, h5, h6',
		'h1'        => 'h1',
		'h2'        => 'h2',
		'h3'        => 'h3',
		'h4'        => 'h4',
		'h5'        => 'h5',
		'h6'        => 'h6',
		// We have the .wp-block-button__link class so that this will target older buttons that have been serialized.
		'button'    => '.wp-element-button, .wp-block-button__link',
		// The block classes are necessary to target older content that won't use the new class names.
		'caption'   => '.wp-element-caption, .wp-block-audio figcaption, .wp-block-embed figcaption, .wp-block-gallery figcaption, .wp-block-image figcaption, .wp-block-table figcaption, .wp-block-video figcaption',
		'cite'      => 'cite',
		'textInput' => 'textarea, input:where([type=email],[type=number],[type=password],[type=search],[type=text],[type=tel],[type=url])',
		'select'    => 'select',
	);

	const __EXPERIMENTAL_ELEMENT_CLASS_NAMES = array(
		'button'  => 'wp-element-button',
		'caption' => 'wp-element-caption',
	);

	/**
	 * List of block support features that can have their related styles
	 * generated under their own feature level selector rather than the block's.
	 *
	 * @since 6.1.0
	 * @var string[]
	 */
	const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = array(
		'__experimentalBorder' => 'border',
		'color'                => 'color',
		'spacing'              => 'spacing',
		'typography'           => 'typography',
	);

	/**
	 * Return the input schema at the root and per origin.
	 *
	 * @since 6.5.0
	 *
	 * @param array $schema The base schema.
	 * @return array The schema at the root and per origin.
	 *
	 * Example:
	 * schema_in_root_and_per_origin(
	 *   array(
	 *    'fontFamily' => null,
	 *    'slug' => null,
	 *   )
	 * )
	 *
	 * Returns:
	 * array(
	 *  'fontFamily' => null,
	 *  'slug' => null,
	 *  'default' => array(
	 *    'fontFamily' => null,
	 *    'slug' => null,
	 *  ),
	 *  'blocks' => array(
	 *    'fontFamily' => null,
	 *    'slug' => null,
	 *  ),
	 *  'theme' => array(
	 *     'fontFamily' => null,
	 *     'slug' => null,
	 *  ),
	 *  'custom' => array(
	 *     'fontFamily' => null,
	 *     'slug' => null,
	 *  ),
	 * )
	 */
	protected static function schema_in_root_and_per_origin( $schema ) {
		$schema_in_root_and_per_origin = $schema;
		foreach ( static::VALID_ORIGINS as $origin ) {
			$schema_in_root_and_per_origin[ $origin ] = $schema;
		}
		return $schema_in_root_and_per_origin;
	}

	/**
	 * Returns a class name by an element name.
	 *
	 * @since 6.1.0
	 *
	 * @param string $element The name of the element.
	 * @return string The name of the class.
	 */
	public static function get_element_class_name( $element ) {
		$class_name = '';

		if ( isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ] ) ) {
			$class_name = static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $element ];
		}

		return $class_name;
	}

	/**
	 * Options that settings.appearanceTools enables.
	 *
	 * @since 6.0.0
	 * @since 6.2.0 Added `dimensions.minHeight` and `position.sticky`.
	 * @since 6.4.0 Added `background.backgroundImage`.
	 * @since 6.5.0 Added `background.backgroundSize` and `dimensions.aspectRatio`.
	 * @var array
	 */
	const APPEARANCE_TOOLS_OPT_INS = array(
		array( 'background', 'backgroundImage' ),
		array( 'background', 'backgroundSize' ),
		array( 'border', 'color' ),
		array( 'border', 'radius' ),
		array( 'border', 'style' ),
		array( 'border', 'width' ),
		array( 'color', 'link' ),
		array( 'color', 'heading' ),
		array( 'color', 'button' ),
		array( 'color', 'caption' ),
		array( 'dimensions', 'aspectRatio' ),
		array( 'dimensions', 'minHeight' ),
		array( 'position', 'sticky' ),
		array( 'spacing', 'blockGap' ),
		array( 'spacing', 'margin' ),
		array( 'spacing', 'padding' ),
		array( 'typography', 'lineHeight' ),
	);

	/**
	 * The latest version of the schema in use.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Changed value from 1 to 2.
	 * @since 6.6.0 Changed value from 2 to 3.
	 * @var int
	 */
	const LATEST_SCHEMA = 3;

	/**
	 * Constructor.
	 *
	 * @since 5.8.0
	 * @since 6.6.0 Key spacingScale by origin, and Pre-generate the spacingSizes from spacingScale.
	 *              Added unwrapping of shared block style variations into block type variations if registered.
	 *
	 * @param array  $theme_json A structure that follows the theme.json schema.
	 * @param string $origin     Optional. What source of data this object represents.
	 *                           One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
	 */
	public function __construct( $theme_json = array( 'version' => self::LATEST_SCHEMA ), $origin = 'theme' ) {
		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
			$origin = 'theme';
		}

		$this->theme_json    = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );
		$blocks_metadata     = static::get_blocks_metadata();
		$valid_block_names   = array_keys( $blocks_metadata );
		$valid_element_names = array_keys( static::ELEMENTS );
		$valid_variations    = static::get_valid_block_style_variations( $blocks_metadata );
		$this->theme_json    = static::unwrap_shared_block_style_variations( $this->theme_json, $valid_variations );
		$this->theme_json    = static::sanitize( $this->theme_json, $valid_block_names, $valid_element_names, $valid_variations );
		$this->theme_json    = static::maybe_opt_in_into_settings( $this->theme_json );

		// Internally, presets are keyed by origin.
		$nodes = static::get_setting_nodes( $this->theme_json );
		foreach ( $nodes as $node ) {
			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
				$path = $node['path'];
				foreach ( $preset_metadata['path'] as $subpath ) {
					$path[] = $subpath;
				}
				$preset = _wp_array_get( $this->theme_json, $path, null );
				if ( null !== $preset ) {
					// If the preset is not already keyed by origin.
					if ( isset( $preset[0] ) || empty( $preset ) ) {
						_wp_array_set( $this->theme_json, $path, array( $origin => $preset ) );
					}
				}
			}
		}

		// In addition to presets, spacingScale (which generates presets) is also keyed by origin.
		$scale_path    = array( 'settings', 'spacing', 'spacingScale' );
		$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
		if ( null !== $spacing_scale ) {
			// If the spacingScale is not already keyed by origin.
			if ( empty( array_intersect( array_keys( $spacing_scale ), static::VALID_ORIGINS ) ) ) {
				_wp_array_set( $this->theme_json, $scale_path, array( $origin => $spacing_scale ) );
			}
		}

		// Pre-generate the spacingSizes from spacingScale.
		$scale_path    = array( 'settings', 'spacing', 'spacingScale', $origin );
		$spacing_scale = _wp_array_get( $this->theme_json, $scale_path, null );
		if ( isset( $spacing_scale ) ) {
			$sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
			$spacing_sizes        = _wp_array_get( $this->theme_json, $sizes_path, array() );
			$spacing_scale_sizes  = static::compute_spacing_sizes( $spacing_scale );
			$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );
			_wp_array_set( $this->theme_json, $sizes_path, $merged_spacing_sizes );
		}
	}

	/**
	 * Unwraps shared block style variations.
	 *
	 * It takes the shared variations (styles.variations.variationName) and
	 * applies them to all the blocks that have the given variation registered
	 * (styles.blocks.blockType.variations.variationName).
	 *
	 * For example, given the `core/paragraph` and `core/group` blocks have
	 * registered the `section-a` style variation, and given the following input:
	 *
	 * {
	 *   "styles": {
	 *     "variations": {
	 *       "section-a": { "color": { "background": "backgroundColor" } }
	 *     }
	 *   }
	 * }
	 *
	 * It returns the following output:
	 *
	 * {
	 *   "styles": {
	 *     "blocks": {
	 *       "core/paragraph": {
	 *         "variations": {
	 *             "section-a": { "color": { "background": "backgroundColor" } }
	 *         },
	 *       },
	 *       "core/group": {
	 *         "variations": {
	 *           "section-a": { "color": { "background": "backgroundColor" } }
	 *         }
	 *       }
	 *     }
	 *   }
	 * }
	 *
	 * @since 6.6.0
	 *
	 * @param array $theme_json       A structure that follows the theme.json schema.
	 * @param array $valid_variations Valid block style variations.
	 * @return array Theme json data with shared variation definitions unwrapped under appropriate block types.
	 */
	private static function unwrap_shared_block_style_variations( $theme_json, $valid_variations ) {
		if ( empty( $theme_json['styles']['variations'] ) || empty( $valid_variations ) ) {
			return $theme_json;
		}

		$new_theme_json = $theme_json;
		$variations     = $new_theme_json['styles']['variations'];

		foreach ( $valid_variations as $block_type => $registered_variations ) {
			foreach ( $registered_variations as $variation_name ) {
				$block_level_data = $new_theme_json['styles']['blocks'][ $block_type ]['variations'][ $variation_name ] ?? array();
				$top_level_data   = $variations[ $variation_name ] ?? array();
				$merged_data      = array_replace_recursive( $top_level_data, $block_level_data );
				if ( ! empty( $merged_data ) ) {
					_wp_array_set( $new_theme_json, array( 'styles', 'blocks', $block_type, 'variations', $variation_name ), $merged_data );
				}
			}
		}

		unset( $new_theme_json['styles']['variations'] );

		return $new_theme_json;
	}

	/**
	 * Enables some opt-in settings if theme declared support.
	 *
	 * @since 5.9.0
	 *
	 * @param array $theme_json A theme.json structure to modify.
	 * @return array The modified theme.json structure.
	 */
	protected static function maybe_opt_in_into_settings( $theme_json ) {
		$new_theme_json = $theme_json;

		if (
			isset( $new_theme_json['settings']['appearanceTools'] ) &&
			true === $new_theme_json['settings']['appearanceTools']
		) {
			static::do_opt_in_into_settings( $new_theme_json['settings'] );
		}

		if ( isset( $new_theme_json['settings']['blocks'] ) && is_array( $new_theme_json['settings']['blocks'] ) ) {
			foreach ( $new_theme_json['settings']['blocks'] as &$block ) {
				if ( isset( $block['appearanceTools'] ) && ( true === $block['appearanceTools'] ) ) {
					static::do_opt_in_into_settings( $block );
				}
			}
		}

		return $new_theme_json;
	}

	/**
	 * Enables some settings.
	 *
	 * @since 5.9.0
	 *
	 * @param array $context The context to which the settings belong.
	 */
	protected static function do_opt_in_into_settings( &$context ) {
		foreach ( static::APPEARANCE_TOOLS_OPT_INS as $path ) {
			/*
			 * Use "unset prop" as a marker instead of "null" because
			 * "null" can be a valid value for some props (e.g. blockGap).
			 */
			if ( 'unset prop' === _wp_array_get( $context, $path, 'unset prop' ) ) {
				_wp_array_set( $context, $path, true );
			}
		}

		unset( $context['appearanceTools'] );
	}

	/**
	 * Sanitizes the input according to the schemas.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `$valid_block_names` and `$valid_element_name` parameters.
	 * @since 6.3.0 Added the `$valid_variations` parameter.
	 * @since 6.6.0 Updated schema to allow extended block style variations.
	 *
	 * @param array $input               Structure to sanitize.
	 * @param array $valid_block_names   List of valid block names.
	 * @param array $valid_element_names List of valid element names.
	 * @param array $valid_variations    List of valid variations per block.
	 * @return array The sanitized output.
	 */
	protected static function sanitize( $input, $valid_block_names, $valid_element_names, $valid_variations ) {
		$output = array();

		if ( ! is_array( $input ) ) {
			return $output;
		}

		// Preserve only the top most level keys.
		$output = array_intersect_key( $input, array_flip( static::VALID_TOP_LEVEL_KEYS ) );

		/*
		 * Remove any rules that are annotated as "top" in VALID_STYLES constant.
		 * Some styles are only meant to be available at the top-level (e.g.: blockGap),
		 * hence, the schema for blocks & elements should not have them.
		 */
		$styles_non_top_level = static::VALID_STYLES;
		foreach ( array_keys( $styles_non_top_level ) as $section ) {
			// array_key_exists() needs to be used instead of isset() because the value can be null.
			if ( array_key_exists( $section, $styles_non_top_level ) && is_array( $styles_non_top_level[ $section ] ) ) {
				foreach ( array_keys( $styles_non_top_level[ $section ] ) as $prop ) {
					if ( 'top' === $styles_non_top_level[ $section ][ $prop ] ) {
						unset( $styles_non_top_level[ $section ][ $prop ] );
					}
				}
			}
		}

		// Build the schema based on valid block & element names.
		$schema                 = array();
		$schema_styles_elements = array();

		/*
		 * Set allowed element pseudo selectors based on per element allow list.
		 * Target data structure in schema:
		 * e.g.
		 * - top level elements: `$schema['styles']['elements']['link'][':hover']`.
		 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`.
		 */
		foreach ( $valid_element_names as $element ) {
			$schema_styles_elements[ $element ] = $styles_non_top_level;

			if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
				foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
					$schema_styles_elements[ $element ][ $pseudo_selector ] = $styles_non_top_level;
				}
			}
		}

		$schema_styles_blocks   = array();
		$schema_settings_blocks = array();

		/*
		 * Generate a schema for blocks.
		 * - Block styles can contain `elements` & `variations` definitions.
		 * - Variations definitions cannot be nested.
		 * - Variations can contain styles for inner `blocks`.
		 * - Variation inner `blocks` styles can contain `elements`.
		 *
		 * As each variation needs a `blocks` schema but further nested
		 * inner `blocks`, the overall schema will be generated in multiple passes.
		 */
		foreach ( $valid_block_names as $block ) {
			$schema_settings_blocks[ $block ]           = static::VALID_SETTINGS;
			$schema_styles_blocks[ $block ]             = $styles_non_top_level;
			$schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements;
		}

		$block_style_variation_styles             = static::VALID_STYLES;
		$block_style_variation_styles['blocks']   = $schema_styles_blocks;
		$block_style_variation_styles['elements'] = $schema_styles_elements;

		foreach ( $valid_block_names as $block ) {
			// Build the schema for each block style variation.
			$style_variation_names = array();
			if (
				! empty( $input['styles']['blocks'][ $block ]['variations'] ) &&
				is_array( $input['styles']['blocks'][ $block ]['variations'] ) &&
				isset( $valid_variations[ $block ] )
			) {
				$style_variation_names = array_intersect(
					array_keys( $input['styles']['blocks'][ $block ]['variations'] ),
					$valid_variations[ $block ]
				);
			}

			$schema_styles_variations = array();
			if ( ! empty( $style_variation_names ) ) {
				$schema_styles_variations = array_fill_keys( $style_variation_names, $block_style_variation_styles );
			}

			$schema_styles_blocks[ $block ]['variations'] = $schema_styles_variations;
		}

		$schema['styles']                                 = static::VALID_STYLES;
		$schema['styles']['blocks']                       = $schema_styles_blocks;
		$schema['styles']['elements']                     = $schema_styles_elements;
		$schema['settings']                               = static::VALID_SETTINGS;
		$schema['settings']['blocks']                     = $schema_settings_blocks;
		$schema['settings']['typography']['fontFamilies'] = static::schema_in_root_and_per_origin( static::FONT_FAMILY_SCHEMA );

		// Remove anything that's not present in the schema.
		foreach ( array( 'styles', 'settings' ) as $subtree ) {
			if ( ! isset( $input[ $subtree ] ) ) {
				continue;
			}

			if ( ! is_array( $input[ $subtree ] ) ) {
				unset( $output[ $subtree ] );
				continue;
			}

			$result = static::remove_keys_not_in_schema( $input[ $subtree ], $schema[ $subtree ] );

			if ( empty( $result ) ) {
				unset( $output[ $subtree ] );
			} else {
				$output[ $subtree ] = static::resolve_custom_css_format( $result );
			}
		}

		return $output;
	}

	/**
	 * Appends a sub-selector to an existing one.
	 *
	 * Given the compounded $selector "h1, h2, h3"
	 * and the $to_append selector ".some-class" the result will be
	 * "h1.some-class, h2.some-class, h3.some-class".
	 *
	 * @since 5.8.0
	 * @since 6.1.0 Added append position.
	 * @since 6.3.0 Removed append position parameter.
	 *
	 * @param string $selector  Original selector.
	 * @param string $to_append Selector to append.
	 * @return string The new selector.
	 */
	protected static function append_to_selector( $selector, $to_append ) {
		if ( ! str_contains( $selector, ',' ) ) {
			return $selector . $to_append;
		}
		$new_selectors = array();
		$selectors     = explode( ',', $selector );
		foreach ( $selectors as $sel ) {
			$new_selectors[] = $sel . $to_append;
		}
		return implode( ',', $new_selectors );
	}

	/**
	 * Prepends a sub-selector to an existing one.
	 *
	 * Given the compounded $selector "h1, h2, h3"
	 * and the $to_prepend selector ".some-class " the result will be
	 * ".some-class h1, .some-class  h2, .some-class  h3".
	 *
	 * @since 6.3.0
	 *
	 * @param string $selector   Original selector.
	 * @param string $to_prepend Selector to prepend.
	 * @return string The new selector.
	 */
	protected static function prepend_to_selector( $selector, $to_prepend ) {
		if ( ! str_contains( $selector, ',' ) ) {
			return $to_prepend . $selector;
		}
		$new_selectors = array();
		$selectors     = explode( ',', $selector );
		foreach ( $selectors as $sel ) {
			$new_selectors[] = $to_prepend . $sel;
		}
		return implode( ',', $new_selectors );
	}

	/**
	 * Returns the metadata for each block.
	 *
	 * Example:
	 *
	 *     {
	 *       'core/paragraph': {
	 *         'selector': 'p',
	 *         'elements': {
	 *           'link' => 'link selector',
	 *           'etc'  => 'element selector'
	 *         }
	 *       },
	 *       'core/heading': {
	 *         'selector': 'h1',
	 *         'elements': {}
	 *       },
	 *       'core/image': {
	 *         'selector': '.wp-block-image',
	 *         'duotone': 'img',
	 *         'elements': {}
	 *       }
	 *     }
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added `duotone` key with CSS selector.
	 * @since 6.1.0 Added `features` key with block support feature level selectors.
	 * @since 6.3.0 Refactored and stabilized selectors API.
	 * @since 6.6.0 Updated to include block style variations from the block styles registry.
	 *
	 * @return array Block metadata.
	 */
	protected static function get_blocks_metadata() {
		$registry       = WP_Block_Type_Registry::get_instance();
		$blocks         = $registry->get_all_registered();
		$style_registry = WP_Block_Styles_Registry::get_instance();

		// Is there metadata for all currently registered blocks?
		$blocks = array_diff_key( $blocks, static::$blocks_metadata );
		if ( empty( $blocks ) ) {
			/*
			 * New block styles may have been registered within WP_Block_Styles_Registry.
			 * Update block metadata for any new block style variations.
			 */
			$registered_styles = $style_registry->get_all_registered();
			foreach ( static::$blocks_metadata as $block_name => $block_metadata ) {
				if ( ! empty( $registered_styles[ $block_name ] ) ) {
					$style_selectors = $block_metadata['styleVariations'] ?? array();

					foreach ( $registered_styles[ $block_name ] as $block_style ) {
						if ( ! isset( $style_selectors[ $block_style['name'] ] ) ) {
							$style_selectors[ $block_style['name'] ] = static::get_block_style_variation_selector( $block_style['name'], $block_metadata['selector'] );
						}
					}

					static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
				}
			}
			return static::$blocks_metadata;
		}

		foreach ( $blocks as $block_name => $block_type ) {
			$root_selector = wp_get_block_css_selector( $block_type );

			static::$blocks_metadata[ $block_name ]['selector']  = $root_selector;
			static::$blocks_metadata[ $block_name ]['selectors'] = static::get_block_selectors( $block_type, $root_selector );

			$elements = static::get_block_element_selectors( $root_selector );
			if ( ! empty( $elements ) ) {
				static::$blocks_metadata[ $block_name ]['elements'] = $elements;
			}

			// The block may or may not have a duotone selector.
			$duotone_selector = wp_get_block_css_selector( $block_type, 'filter.duotone' );

			// Keep backwards compatibility for support.color.__experimentalDuotone.
			if ( null === $duotone_selector ) {
				$duotone_support = isset( $block_type->supports['color']['__experimentalDuotone'] )
					? $block_type->supports['color']['__experimentalDuotone']
					: null;

				if ( $duotone_support ) {
					$root_selector    = wp_get_block_css_selector( $block_type );
					$duotone_selector = static::scope_selector( $root_selector, $duotone_support );
				}
			}

			if ( null !== $duotone_selector ) {
				static::$blocks_metadata[ $block_name ]['duotone'] = $duotone_selector;
			}

			// If the block has style variations, append their selectors to the block metadata.
			$style_selectors = array();
			if ( ! empty( $block_type->styles ) ) {
				foreach ( $block_type->styles as $style ) {
					$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
				}
			}

			// Block style variations can be registered through the WP_Block_Styles_Registry as well as block.json.
			$registered_styles = $style_registry->get_registered_styles_for_block( $block_name );
			foreach ( $registered_styles as $style ) {
				$style_selectors[ $style['name'] ] = static::get_block_style_variation_selector( $style['name'], static::$blocks_metadata[ $block_name ]['selector'] );
			}

			if ( ! empty( $style_selectors ) ) {
				static::$blocks_metadata[ $block_name ]['styleVariations'] = $style_selectors;
			}
		}

		return static::$blocks_metadata;
	}

	/**
	 * Given a tree, removes the keys that are not present in the schema.
	 *
	 * It is recursive and modifies the input in-place.
	 *
	 * @since 5.8.0
	 *
	 * @param array $tree   Input to process.
	 * @param array $schema Schema to adhere to.
	 * @return array The modified $tree.
	 */
	protected static function remove_keys_not_in_schema( $tree, $schema ) {
		if ( ! is_array( $tree ) ) {
			return $tree;
		}

		foreach ( $tree as $key => $value ) {
			// Remove keys not in the schema or with null/empty values.
			if ( ! array_key_exists( $key, $schema ) ) {
				unset( $tree[ $key ] );
				continue;
			}

			if ( is_array( $schema[ $key ] ) ) {
				if ( ! is_array( $value ) ) {
					unset( $tree[ $key ] );
				} elseif ( wp_is_numeric_array( $value ) ) {
					// If indexed, process each item in the array.
					foreach ( $value as $item_key => $item_value ) {
						if ( isset( $schema[ $key ][0] ) && is_array( $schema[ $key ][0] ) ) {
							$tree[ $key ][ $item_key ] = self::remove_keys_not_in_schema( $item_value, $schema[ $key ][0] );
						} else {
							// If the schema does not define a further structure, keep the value as is.
							$tree[ $key ][ $item_key ] = $item_value;
						}
					}
				} else {
					// If associative, process as a single object.
					$tree[ $key ] = self::remove_keys_not_in_schema( $value, $schema[ $key ] );

					if ( empty( $tree[ $key ] ) ) {
						unset( $tree[ $key ] );
					}
				}
			}
		}
		return $tree;
	}

	/**
	 * Returns the existing settings for each block.
	 *
	 * Example:
	 *
	 *     {
	 *       'root': {
	 *         'color': {
	 *           'custom': true
	 *         }
	 *       },
	 *       'core/paragraph': {
	 *         'spacing': {
	 *           'customPadding': true
	 *         }
	 *       }
	 *     }
	 *
	 * @since 5.8.0
	 *
	 * @return array Settings per block.
	 */
	public function get_settings() {
		if ( ! isset( $this->theme_json['settings'] ) ) {
			return array();
		} else {
			return $this->theme_json['settings'];
		}
	}

	/**
	 * Returns the stylesheet that results of processing
	 * the theme.json structure this object represents.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Removed the `$type` parameter, added the `$types` and `$origins` parameters.
	 * @since 6.3.0 Add fallback layout styles for Post Template when block gap support isn't available.
	 * @since 6.6.0 Added boolean `skip_root_layout_styles` and `include_block_style_variations` options
	 *              to control styles output as desired.
	 *
	 * @param string[] $types   Types of styles to load. Will load all by default. It accepts:
	 *                          - `variables`: only the CSS Custom Properties for presets & custom ones.
	 *                          - `styles`: only the styles section in theme.json.
	 *                          - `presets`: only the classes for the presets.
	 *                          - `base-layout-styles`: only the base layout styles.
	 *                          - `custom-css`: only the custom CSS.
	 * @param string[] $origins A list of origins to include. By default it includes VALID_ORIGINS.
	 * @param array    $options {
	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
	 *
	 *     @type string $scope                           Makes sure all style are scoped to a given selector
	 *     @type string $root_selector                   Overwrites and forces a given selector to be used on the root node
	 *     @type bool   $skip_root_layout_styles         Omits root layout styles from the generated stylesheet. Default false.
	 *     @type bool   $include_block_style_variations  Includes styles for block style variations in the generated stylesheet. Default false.
	 * }
	 * @return string The resulting stylesheet.
	 */
	public function get_stylesheet( $types = array( 'variables', 'styles', 'presets' ), $origins = null, $options = array() ) {
		if ( null === $origins ) {
			$origins = static::VALID_ORIGINS;
		}

		if ( is_string( $types ) ) {
			// Dispatch error and map old arguments to new ones.
			_deprecated_argument( __FUNCTION__, '5.9.0' );
			if ( 'block_styles' === $types ) {
				$types = array( 'styles', 'presets' );
			} elseif ( 'css_variables' === $types ) {
				$types = array( 'variables' );
			} else {
				$types = array( 'variables', 'styles', 'presets' );
			}
		}

		$blocks_metadata = static::get_blocks_metadata();
		$style_nodes     = static::get_style_nodes( $this->theme_json, $blocks_metadata, $options );
		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

		$root_style_key    = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $style_nodes, 'selector' ), true );
		$root_settings_key = array_search( static::ROOT_BLOCK_SELECTOR, array_column( $setting_nodes, 'selector' ), true );

		if ( ! empty( $options['scope'] ) ) {
			foreach ( $setting_nodes as &$node ) {
				$node['selector'] = static::scope_selector( $options['scope'], $node['selector'] );
			}
			foreach ( $style_nodes as &$node ) {
				$node = static::scope_style_node_selectors( $options['scope'], $node );
			}
			unset( $node );
		}

		if ( ! empty( $options['root_selector'] ) ) {
			if ( false !== $root_settings_key ) {
				$setting_nodes[ $root_settings_key ]['selector'] = $options['root_selector'];
			}
			if ( false !== $root_style_key ) {
				$style_nodes[ $root_style_key ]['selector'] = $options['root_selector'];
			}
		}

		$stylesheet = '';

		if ( in_array( 'variables', $types, true ) ) {
			$stylesheet .= $this->get_css_variables( $setting_nodes, $origins );
		}

		if ( in_array( 'styles', $types, true ) ) {
			if ( false !== $root_style_key && empty( $options['skip_root_layout_styles'] ) ) {
				$stylesheet .= $this->get_root_layout_rules( $style_nodes[ $root_style_key ]['selector'], $style_nodes[ $root_style_key ] );
			}
			$stylesheet .= $this->get_block_classes( $style_nodes );
		} elseif ( in_array( 'base-layout-styles', $types, true ) ) {
			$root_selector          = static::ROOT_BLOCK_SELECTOR;
			$columns_selector       = '.wp-block-columns';
			$post_template_selector = '.wp-block-post-template';
			if ( ! empty( $options['scope'] ) ) {
				$root_selector          = static::scope_selector( $options['scope'], $root_selector );
				$columns_selector       = static::scope_selector( $options['scope'], $columns_selector );
				$post_template_selector = static::scope_selector( $options['scope'], $post_template_selector );
			}
			if ( ! empty( $options['root_selector'] ) ) {
				$root_selector = $options['root_selector'];
			}
			/*
			 * Base layout styles are provided as part of `styles`, so only output separately if explicitly requested.
			 * For backwards compatibility, the Columns block is explicitly included, to support a different default gap value.
			 */
			$base_styles_nodes = array(
				array(
					'path'     => array( 'styles' ),
					'selector' => $root_selector,
				),
				array(
					'path'     => array( 'styles', 'blocks', 'core/columns' ),
					'selector' => $columns_selector,
					'name'     => 'core/columns',
				),
				array(
					'path'     => array( 'styles', 'blocks', 'core/post-template' ),
					'selector' => $post_template_selector,
					'name'     => 'core/post-template',
				),
			);

			foreach ( $base_styles_nodes as $base_style_node ) {
				$stylesheet .= $this->get_layout_styles( $base_style_node, $types );
			}
		}

		if ( in_array( 'presets', $types, true ) ) {
			$stylesheet .= $this->get_preset_classes( $setting_nodes, $origins );
		}

		// Load the custom CSS last so it has the highest specificity.
		if ( in_array( 'custom-css', $types, true ) ) {
			// Add the global styles root CSS.
			$stylesheet .= _wp_array_get( $this->theme_json, array( 'styles', 'css' ) );
		}

		return $stylesheet;
	}

	/**
	 * Processes the CSS, to apply nesting.
	 *
	 * @since 6.2.0
	 * @since 6.6.0 Enforced 0-1-0 specificity for block custom CSS selectors.
	 *
	 * @param string $css      The CSS to process.
	 * @param string $selector The selector to nest.
	 * @return string The processed CSS.
	 */
	protected function process_blocks_custom_css( $css, $selector ) {
		$processed_css = '';

		if ( empty( $css ) ) {
			return $processed_css;
		}

		// Split CSS nested rules.
		$parts = explode( '&', $css );
		foreach ( $parts as $part ) {
			if ( empty( $part ) ) {
				continue;
			}
			$is_root_css = ( ! str_contains( $part, '{' ) );
			if ( $is_root_css ) {
				// If the part doesn't contain braces, it applies to the root level.
				$processed_css .= ':root :where(' . trim( $selector ) . '){' . trim( $part ) . '}';
			} else {
				// If the part contains braces, it's a nested CSS rule.
				$part = explode( '{', str_replace( '}', '', $part ) );
				if ( count( $part ) !== 2 ) {
					continue;
				}
				$nested_selector = $part[0];
				$css_value       = $part[1];

				/*
				 * Handle pseudo elements such as ::before, ::after etc. Regex will also
				 * capture any leading combinator such as >, +, or ~, as well as spaces.
				 * This allows pseudo elements as descendants e.g. `.parent ::before`.
				 */
				$matches            = array();
				$has_pseudo_element = preg_match( '/([>+~\s]*::[a-zA-Z-]+)/', $nested_selector, $matches );
				$pseudo_part        = $has_pseudo_element ? $matches[1] : '';
				$nested_selector    = $has_pseudo_element ? str_replace( $pseudo_part, '', $nested_selector ) : $nested_selector;

				// Finalize selector and re-append pseudo element if required.
				$part_selector  = str_starts_with( $nested_selector, ' ' )
					? static::scope_selector( $selector, $nested_selector )
					: static::append_to_selector( $selector, $nested_selector );
				$final_selector = ":root :where($part_selector)$pseudo_part";

				$processed_css .= $final_selector . '{' . trim( $css_value ) . '}';
			}
		}
		return $processed_css;
	}

	/**
	 * Returns the global styles custom CSS.
	 *
	 * @since 6.2.0
	 * @deprecated 6.7.0 Use {@see 'get_stylesheet'} instead.
	 *
	 * @return string The global styles custom CSS.
	 */
	public function get_custom_css() {
		_deprecated_function( __METHOD__, '6.7.0', 'get_stylesheet' );
		// Add the global styles root CSS.
		$stylesheet = isset( $this->theme_json['styles']['css'] ) ? $this->theme_json['styles']['css'] : '';

		// Add the global styles block CSS.
		if ( isset( $this->theme_json['styles']['blocks'] ) ) {
			foreach ( $this->theme_json['styles']['blocks'] as $name => $node ) {
				$custom_block_css = isset( $this->theme_json['styles']['blocks'][ $name ]['css'] )
					? $this->theme_json['styles']['blocks'][ $name ]['css']
					: null;
				if ( $custom_block_css ) {
					$selector    = static::$blocks_metadata[ $name ]['selector'];
					$stylesheet .= $this->process_blocks_custom_css( $custom_block_css, $selector );
				}
			}
		}

		return $stylesheet;
	}

	/**
	 * Returns the page templates of the active theme.
	 *
	 * @since 5.9.0
	 *
	 * @return array
	 */
	public function get_custom_templates() {
		$custom_templates = array();
		if ( ! isset( $this->theme_json['customTemplates'] ) || ! is_array( $this->theme_json['customTemplates'] ) ) {
			return $custom_templates;
		}

		foreach ( $this->theme_json['customTemplates'] as $item ) {
			if ( isset( $item['name'] ) ) {
				$custom_templates[ $item['name'] ] = array(
					'title'     => isset( $item['title'] ) ? $item['title'] : '',
					'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ),
				);
			}
		}
		return $custom_templates;
	}

	/**
	 * Returns the template part data of active theme.
	 *
	 * @since 5.9.0
	 *
	 * @return array
	 */
	public function get_template_parts() {
		$template_parts = array();
		if ( ! isset( $this->theme_json['templateParts'] ) || ! is_array( $this->theme_json['templateParts'] ) ) {
			return $template_parts;
		}

		foreach ( $this->theme_json['templateParts'] as $item ) {
			if ( isset( $item['name'] ) ) {
				$template_parts[ $item['name'] ] = array(
					'title' => isset( $item['title'] ) ? $item['title'] : '',
					'area'  => isset( $item['area'] ) ? $item['area'] : '',
				);
			}
		}
		return $template_parts;
	}

	/**
	 * Converts each style section into a list of rulesets
	 * containing the block styles to be appended to the stylesheet.
	 *
	 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
	 *
	 * For each section this creates a new ruleset such as:
	 *
	 *   block-selector {
	 *     style-property-one: value;
	 *   }
	 *
	 * @since 5.8.0 As `get_block_styles()`.
	 * @since 5.9.0 Renamed from `get_block_styles()` to `get_block_classes()`
	 *              and no longer returns preset classes.
	 *              Removed the `$setting_nodes` parameter.
	 * @since 6.1.0 Moved most internal logic to `get_styles_for_block()`.
	 *
	 * @param array $style_nodes Nodes with styles.
	 * @return string The new stylesheet.
	 */
	protected function get_block_classes( $style_nodes ) {
		$block_rules = '';

		foreach ( $style_nodes as $metadata ) {
			if ( null === $metadata['selector'] ) {
				continue;
			}
			$block_rules .= static::get_styles_for_block( $metadata );
		}

		return $block_rules;
	}

	/**
	 * Gets the CSS layout rules for a particular block from theme.json layout definitions.
	 *
	 * @since 6.1.0
	 * @since 6.3.0 Reduced specificity for layout margin rules.
	 * @since 6.5.1 Only output rules referencing content and wide sizes when values exist.
	 * @since 6.5.3 Add types parameter to check if only base layout styles are needed.
	 * @since 6.6.0 Updated layout style specificity to be compatible with overall 0-1-0 specificity in global styles.
	 *
	 * @param array $block_metadata Metadata about the block to get styles for.
	 * @param array $types          Optional. Types of styles to output. If empty, all styles will be output.
	 * @return string Layout styles for the block.
	 */
	protected function get_layout_styles( $block_metadata, $types = array() ) {
		$block_rules = '';
		$block_type  = null;

		// Skip outputting layout styles if explicitly disabled.
		if ( current_theme_supports( 'disable-layout-styles' ) ) {
			return $block_rules;
		}

		if ( isset( $block_metadata['name'] ) ) {
			$block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block_metadata['name'] );
			if ( ! block_has_support( $block_type, 'layout', false ) && ! block_has_support( $block_type, '__experimentalLayout', false ) ) {
				return $block_rules;
			}
		}

		$selector                 = isset( $block_metadata['selector'] ) ? $block_metadata['selector'] : '';
		$has_block_gap_support    = isset( $this->theme_json['settings']['spacing']['blockGap'] );
		$has_fallback_gap_support = ! $has_block_gap_support; // This setting isn't useful yet: it exists as a placeholder for a future explicit fallback gap styles support.
		$node                     = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
		$layout_definitions       = wp_get_layout_definitions();
		$layout_selector_pattern  = '/^[a-zA-Z0-9\-\.\,\ *+>:\(\)]*$/'; // Allow alphanumeric classnames, spaces, wildcard, sibling, child combinator and pseudo class selectors.

		/*
		 * Gap styles will only be output if the theme has block gap support, or supports a fallback gap.
		 * Default layout gap styles will be skipped for themes that do not explicitly opt-in to blockGap with a `true` or `false` value.
		 */
		if ( $has_block_gap_support || $has_fallback_gap_support ) {
			$block_gap_value = null;
			// Use a fallback gap value if block gap support is not available.
			if ( ! $has_block_gap_support ) {
				$block_gap_value = static::ROOT_BLOCK_SELECTOR === $selector ? '0.5em' : null;
				if ( ! empty( $block_type ) ) {
					$block_gap_value = isset( $block_type->supports['spacing']['blockGap']['__experimentalDefault'] )
						? $block_type->supports['spacing']['blockGap']['__experimentalDefault']
						: null;
				}
			} else {
				$block_gap_value = static::get_property_value( $node, array( 'spacing', 'blockGap' ) );
			}

			// Support split row / column values and concatenate to a shorthand value.
			if ( is_array( $block_gap_value ) ) {
				if ( isset( $block_gap_value['top'] ) && isset( $block_gap_value['left'] ) ) {
					$gap_row         = static::get_property_value( $node, array( 'spacing', 'blockGap', 'top' ) );
					$gap_column      = static::get_property_value( $node, array( 'spacing', 'blockGap', 'left' ) );
					$block_gap_value = $gap_row === $gap_column ? $gap_row : $gap_row . ' ' . $gap_column;
				} else {
					// Skip outputting gap value if not all sides are provided.
					$block_gap_value = null;
				}
			}

			// If the block should have custom gap, add the gap styles.
			if ( null !== $block_gap_value && false !== $block_gap_value && '' !== $block_gap_value ) {
				foreach ( $layout_definitions as $layout_definition_key => $layout_definition ) {
					// Allow outputting fallback gap styles for flex and grid layout types when block gap support isn't available.
					if ( ! $has_block_gap_support && 'flex' !== $layout_definition_key && 'grid' !== $layout_definition_key ) {
						continue;
					}

					$class_name    = isset( $layout_definition['className'] ) ? $layout_definition['className'] : false;
					$spacing_rules = isset( $layout_definition['spacingStyles'] ) ? $layout_definition['spacingStyles'] : array();

					if (
						! empty( $class_name ) &&
						! empty( $spacing_rules )
					) {
						foreach ( $spacing_rules as $spacing_rule ) {
							$declarations = array();
							if (
								isset( $spacing_rule['selector'] ) &&
								preg_match( $layout_selector_pattern, $spacing_rule['selector'] ) &&
								! empty( $spacing_rule['rules'] )
							) {
								// Iterate over each of the styling rules and substitute non-string values such as `null` with the real `blockGap` value.
								foreach ( $spacing_rule['rules'] as $css_property => $css_value ) {
									$current_css_value = is_string( $css_value ) ? $css_value : $block_gap_value;
									if ( static::is_safe_css_declaration( $css_property, $current_css_value ) ) {
										$declarations[] = array(
											'name'  => $css_property,
											'value' => $current_css_value,
										);
									}
								}

								if ( ! $has_block_gap_support ) {
									// For fallback gap styles, use lower specificity, to ensure styles do not unintentionally override theme styles.
									$format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':where(.%2$s%3$s)' : ':where(%1$s.%2$s%3$s)';
									$layout_selector = sprintf(
										$format,
										$selector,
										$class_name,
										$spacing_rule['selector']
									);
								} else {
									$format          = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s';
									$layout_selector = sprintf(
										$format,
										$selector,
										$class_name,
										$spacing_rule['selector']
									);
								}
								$block_rules .= static::to_ruleset( $layout_selector, $declarations );
							}
						}
					}
				}
			}
		}

		// Output base styles.
		if (
			static::ROOT_BLOCK_SELECTOR === $selector
		) {
			$valid_display_modes = array( 'block', 'flex', 'grid' );
			foreach ( $layout_definitions as $layout_definition ) {
				$class_name       = isset( $layout_definition['className'] ) ? $layout_definition['className'] : false;
				$base_style_rules = isset( $layout_definition['baseStyles'] ) ? $layout_definition['baseStyles'] : array();

				if (
					! empty( $class_name ) &&
					is_array( $base_style_rules )
				) {
					// Output display mode. This requires special handling as `display` is not exposed in `safe_style_css_filter`.
					if (
						! empty( $layout_definition['displayMode'] ) &&
						is_string( $layout_definition['displayMode'] ) &&
						in_array( $layout_definition['displayMode'], $valid_display_modes, true )
					) {
						$layout_selector = sprintf(
							'%s .%s',
							$selector,
							$class_name
						);
						$block_rules    .= static::to_ruleset(
							$layout_selector,
							array(
								array(
									'name'  => 'display',
									'value' => $layout_definition['displayMode'],
								),
							)
						);
					}

					foreach ( $base_style_rules as $base_style_rule ) {
						$declarations = array();

						// Skip outputting base styles for flow and constrained layout types if theme doesn't support theme.json. The 'base-layout-styles' type flags this.
						if ( in_array( 'base-layout-styles', $types, true ) && ( 'default' === $layout_definition['name'] || 'constrained' === $layout_definition['name'] ) ) {
							continue;
						}

						if (
							isset( $base_style_rule['selector'] ) &&
							preg_match( $layout_selector_pattern, $base_style_rule['selector'] ) &&
							! empty( $base_style_rule['rules'] )
						) {
							foreach ( $base_style_rule['rules'] as $css_property => $css_value ) {
								// Skip rules that reference content size or wide size if they are not defined in the theme.json.
								if (
									is_string( $css_value ) &&
									( str_contains( $css_value, '--global--content-size' ) || str_contains( $css_value, '--global--wide-size' ) ) &&
									! isset( $this->theme_json['settings']['layout']['contentSize'] ) &&
									! isset( $this->theme_json['settings']['layout']['wideSize'] )
								) {
									continue;
								}

								if ( static::is_safe_css_declaration( $css_property, $css_value ) ) {
									$declarations[] = array(
										'name'  => $css_property,
										'value' => $css_value,
									);
								}
							}

							$layout_selector = sprintf(
								'.%s%s',
								$class_name,
								$base_style_rule['selector']
							);
							$block_rules    .= static::to_ruleset( $layout_selector, $declarations );
						}
					}
				}
			}
		}
		return $block_rules;
	}

	/**
	 * Creates new rulesets as classes for each preset value such as:
	 *
	 *   .has-value-color {
	 *     color: value;
	 *   }
	 *
	 *   .has-value-background-color {
	 *     background-color: value;
	 *   }
	 *
	 *   .has-value-font-size {
	 *     font-size: value;
	 *   }
	 *
	 *   .has-value-gradient-background {
	 *     background: value;
	 *   }
	 *
	 *   p.has-value-gradient-background {
	 *     background: value;
	 *   }
	 *
	 * @since 5.9.0
	 *
	 * @param array    $setting_nodes Nodes with settings.
	 * @param string[] $origins       List of origins to process presets from.
	 * @return string The new stylesheet.
	 */
	protected function get_preset_classes( $setting_nodes, $origins ) {
		$preset_rules = '';

		foreach ( $setting_nodes as $metadata ) {
			if ( null === $metadata['selector'] ) {
				continue;
			}

			$selector      = $metadata['selector'];
			$node          = _wp_array_get( $this->theme_json, $metadata['path'], array() );
			$preset_rules .= static::compute_preset_classes( $node, $selector, $origins );
		}

		return $preset_rules;
	}

	/**
	 * Converts each styles section into a list of rulesets
	 * to be appended to the stylesheet.
	 * These rulesets contain all the css variables (custom variables and preset variables).
	 *
	 * See glossary at https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax
	 *
	 * For each section this creates a new ruleset such as:
	 *
	 *     block-selector {
	 *       --wp--preset--category--slug: value;
	 *       --wp--custom--variable: value;
	 *     }
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `$origins` parameter.
	 *
	 * @param array    $nodes   Nodes with settings.
	 * @param string[] $origins List of origins to process.
	 * @return string The new stylesheet.
	 */
	protected function get_css_variables( $nodes, $origins ) {
		$stylesheet = '';
		foreach ( $nodes as $metadata ) {
			if ( null === $metadata['selector'] ) {
				continue;
			}

			$selector = $metadata['selector'];

			$node                    = _wp_array_get( $this->theme_json, $metadata['path'], array() );
			$declarations            = static::compute_preset_vars( $node, $origins );
			$theme_vars_declarations = static::compute_theme_vars( $node );
			foreach ( $theme_vars_declarations as $theme_vars_declaration ) {
				$declarations[] = $theme_vars_declaration;
			}

			$stylesheet .= static::to_ruleset( $selector, $declarations );
		}

		return $stylesheet;
	}

	/**
	 * Given a selector and a declaration list,
	 * creates the corresponding ruleset.
	 *
	 * @since 5.8.0
	 *
	 * @param string $selector     CSS selector.
	 * @param array  $declarations List of declarations.
	 * @return string The resulting CSS ruleset.
	 */
	protected static function to_ruleset( $selector, $declarations ) {
		if ( empty( $declarations ) ) {
			return '';
		}

		$declaration_block = array_reduce(
			$declarations,
			static function ( $carry, $element ) {
				return $carry .= $element['name'] . ': ' . $element['value'] . ';'; },
			''
		);

		return $selector . '{' . $declaration_block . '}';
	}

	/**
	 * Given a settings array, returns the generated rulesets
	 * for the preset classes.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `$origins` parameter.
	 * @since 6.6.0 Added check for root CSS properties selector.
	 *
	 * @param array    $settings Settings to process.
	 * @param string   $selector Selector wrapping the classes.
	 * @param string[] $origins  List of origins to process.
	 * @return string The result of processing the presets.
	 */
	protected static function compute_preset_classes( $settings, $selector, $origins ) {
		if ( static::ROOT_BLOCK_SELECTOR === $selector || static::ROOT_CSS_PROPERTIES_SELECTOR === $selector ) {
			/*
			 * Classes at the global level do not need any CSS prefixed,
			 * and we don't want to increase its specificity.
			 */
			$selector = '';
		}

		$stylesheet = '';
		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
			if ( empty( $preset_metadata['classes'] ) ) {
				continue;
			}
			$slugs = static::get_settings_slugs( $settings, $preset_metadata, $origins );
			foreach ( $preset_metadata['classes'] as $class => $property ) {
				foreach ( $slugs as $slug ) {
					$css_var    = static::replace_slug_in_string( $preset_metadata['css_vars'], $slug );
					$class_name = static::replace_slug_in_string( $class, $slug );

					// $selector is often empty, so we can save ourselves the `append_to_selector()` call then.
					$new_selector = '' === $selector ? $class_name : static::append_to_selector( $selector, $class_name );
					$stylesheet  .= static::to_ruleset(
						$new_selector,
						array(
							array(
								'name'  => $property,
								'value' => 'var(' . $css_var . ') !important',
							),
						)
					);
				}
			}
		}

		return $stylesheet;
	}

	/**
	 * Function that scopes a selector with another one. This works a bit like
	 * SCSS nesting except the `&` operator isn't supported.
	 *
	 * <code>
	 * $scope = '.a, .b .c';
	 * $selector = '> .x, .y';
	 * $merged = scope_selector( $scope, $selector );
	 * // $merged is '.a > .x, .a .y, .b .c > .x, .b .c .y'
	 * </code>
	 *
	 * @since 5.9.0
	 * @since 6.6.0 Added early return if missing scope or selector.
	 *
	 * @param string $scope    Selector to scope to.
	 * @param string $selector Original selector.
	 * @return string Scoped selector.
	 */
	public static function scope_selector( $scope, $selector ) {
		if ( ! $scope || ! $selector ) {
			return $selector;
		}

		$scopes    = explode( ',', $scope );
		$selectors = explode( ',', $selector );

		$selectors_scoped = array();
		foreach ( $scopes as $outer ) {
			foreach ( $selectors as $inner ) {
				$outer = trim( $outer );
				$inner = trim( $inner );
				if ( ! empty( $outer ) && ! empty( $inner ) ) {
					$selectors_scoped[] = $outer . ' ' . $inner;
				} elseif ( empty( $outer ) ) {
					$selectors_scoped[] = $inner;
				} elseif ( empty( $inner ) ) {
					$selectors_scoped[] = $outer;
				}
			}
		}

		$result = implode( ', ', $selectors_scoped );
		return $result;
	}

	/**
	 * Scopes the selectors for a given style node.
	 *
	 * This includes the primary selector, i.e. `$node['selector']`, as well as any custom
	 * selectors for features and subfeatures, e.g. `$node['selectors']['border']` etc.
	 *
	 * @since 6.6.0
	 *
	 * @param string $scope Selector to scope to.
	 * @param array  $node  Style node with selectors to scope.
	 * @return array Node with updated selectors.
	 */
	protected static function scope_style_node_selectors( $scope, $node ) {
		$node['selector'] = static::scope_selector( $scope, $node['selector'] );

		if ( empty( $node['selectors'] ) ) {
			return $node;
		}

		foreach ( $node['selectors'] as $feature => $selector ) {
			if ( is_string( $selector ) ) {
				$node['selectors'][ $feature ] = static::scope_selector( $scope, $selector );
			}
			if ( is_array( $selector ) ) {
				foreach ( $selector as $subfeature => $subfeature_selector ) {
					$node['selectors'][ $feature ][ $subfeature ] = static::scope_selector( $scope, $subfeature_selector );
				}
			}
		}

		return $node;
	}

	/**
	 * Gets preset values keyed by slugs based on settings and metadata.
	 *
	 * <code>
	 * $settings = array(
	 *     'typography' => array(
	 *         'fontFamilies' => array(
	 *             array(
	 *                 'slug'       => 'sansSerif',
	 *                 'fontFamily' => '"Helvetica Neue", sans-serif',
	 *             ),
	 *             array(
	 *                 'slug'   => 'serif',
	 *                 'colors' => 'Georgia, serif',
	 *             )
	 *         ),
	 *     ),
	 * );
	 * $meta = array(
	 *    'path'      => array( 'typography', 'fontFamilies' ),
	 *    'value_key' => 'fontFamily',
	 * );
	 * $values_by_slug = get_settings_values_by_slug();
	 * // $values_by_slug === array(
	 * //   'sans-serif' => '"Helvetica Neue", sans-serif',
	 * //   'serif'      => 'Georgia, serif',
	 * // );
	 * </code>
	 *
	 * @since 5.9.0
	 * @since 6.6.0 Passing $settings to the callbacks defined in static::PRESETS_METADATA.
	 *
	 * @param array    $settings        Settings to process.
	 * @param array    $preset_metadata One of the PRESETS_METADATA values.
	 * @param string[] $origins         List of origins to process.
	 * @return array Array of presets where each key is a slug and each value is the preset value.
	 */
	protected static function get_settings_values_by_slug( $settings, $preset_metadata, $origins ) {
		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );

		$result = array();
		foreach ( $origins as $origin ) {
			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
				continue;
			}
			foreach ( $preset_per_origin[ $origin ] as $preset ) {
				$slug = _wp_to_kebab_case( $preset['slug'] );

				$value = '';
				if ( isset( $preset_metadata['value_key'], $preset[ $preset_metadata['value_key'] ] ) ) {
					$value_key = $preset_metadata['value_key'];
					$value     = $preset[ $value_key ];
				} elseif (
					isset( $preset_metadata['value_func'] ) &&
					is_callable( $preset_metadata['value_func'] )
				) {
					$value_func = $preset_metadata['value_func'];
					$value      = call_user_func( $value_func, $preset, $settings );
				} else {
					// If we don't have a value, then don't add it to the result.
					continue;
				}

				$result[ $slug ] = $value;
			}
		}
		return $result;
	}

	/**
	 * Similar to get_settings_values_by_slug, but doesn't compute the value.
	 *
	 * @since 5.9.0
	 *
	 * @param array    $settings        Settings to process.
	 * @param array    $preset_metadata One of the PRESETS_METADATA values.
	 * @param string[] $origins         List of origins to process.
	 * @return array Array of presets where the key and value are both the slug.
	 */
	protected static function get_settings_slugs( $settings, $preset_metadata, $origins = null ) {
		if ( null === $origins ) {
			$origins = static::VALID_ORIGINS;
		}

		$preset_per_origin = _wp_array_get( $settings, $preset_metadata['path'], array() );

		$result = array();
		foreach ( $origins as $origin ) {
			if ( ! isset( $preset_per_origin[ $origin ] ) ) {
				continue;
			}
			foreach ( $preset_per_origin[ $origin ] as $preset ) {
				$slug = _wp_to_kebab_case( $preset['slug'] );

				// Use the array as a set so we don't get duplicates.
				$result[ $slug ] = $slug;
			}
		}
		return $result;
	}

	/**
	 * Transforms a slug into a CSS Custom Property.
	 *
	 * @since 5.9.0
	 *
	 * @param string $input String to replace.
	 * @param string $slug  The slug value to use to generate the custom property.
	 * @return string The CSS Custom Property. Something along the lines of `--wp--preset--color--black`.
	 */
	protected static function replace_slug_in_string( $input, $slug ) {
		return strtr( $input, array( '$slug' => $slug ) );
	}

	/**
	 * Given the block settings, extracts the CSS Custom Properties
	 * for the presets and adds them to the $declarations array
	 * following the format:
	 *
	 *     array(
	 *       'name'  => 'property_name',
	 *       'value' => 'property_value,
	 *     )
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `$origins` parameter.
	 *
	 * @param array    $settings Settings to process.
	 * @param string[] $origins  List of origins to process.
	 * @return array The modified $declarations.
	 */
	protected static function compute_preset_vars( $settings, $origins ) {
		$declarations = array();
		foreach ( static::PRESETS_METADATA as $preset_metadata ) {
			if ( empty( $preset_metadata['css_vars'] ) ) {
				continue;
			}
			$values_by_slug = static::get_settings_values_by_slug( $settings, $preset_metadata, $origins );
			foreach ( $values_by_slug as $slug => $value ) {
				$declarations[] = array(
					'name'  => static::replace_slug_in_string( $preset_metadata['css_vars'], $slug ),
					'value' => $value,
				);
			}
		}

		return $declarations;
	}

	/**
	 * Given an array of settings, extracts the CSS Custom Properties
	 * for the custom values and adds them to the $declarations
	 * array following the format:
	 *
	 *     array(
	 *       'name'  => 'property_name',
	 *       'value' => 'property_value,
	 *     )
	 *
	 * @since 5.8.0
	 *
	 * @param array $settings Settings to process.
	 * @return array The modified $declarations.
	 */
	protected static function compute_theme_vars( $settings ) {
		$declarations  = array();
		$custom_values = isset( $settings['custom'] ) ? $settings['custom'] : array();
		$css_vars      = static::flatten_tree( $custom_values );
		foreach ( $css_vars as $key => $value ) {
			$declarations[] = array(
				'name'  => '--wp--custom--' . $key,
				'value' => $value,
			);
		}

		return $declarations;
	}

	/**
	 * Given a tree, it creates a flattened one
	 * by merging the keys and binding the leaf values
	 * to the new keys.
	 *
	 * It also transforms camelCase names into kebab-case
	 * and substitutes '/' by '-'.
	 *
	 * This is thought to be useful to generate
	 * CSS Custom Properties from a tree,
	 * although there's nothing in the implementation
	 * of this function that requires that format.
	 *
	 * For example, assuming the given prefix is '--wp'
	 * and the token is '--', for this input tree:
	 *
	 *     {
	 *       'some/property': 'value',
	 *       'nestedProperty': {
	 *         'sub-property': 'value'
	 *       }
	 *     }
	 *
	 * it'll return this output:
	 *
	 *     {
	 *       '--wp--some-property': 'value',
	 *       '--wp--nested-property--sub-property': 'value'
	 *     }
	 *
	 * @since 5.8.0
	 *
	 * @param array  $tree   Input tree to process.
	 * @param string $prefix Optional. Prefix to prepend to each variable. Default empty string.
	 * @param string $token  Optional. Token to use between levels. Default '--'.
	 * @return array The flattened tree.
	 */
	protected static function flatten_tree( $tree, $prefix = '', $token = '--' ) {
		$result = array();
		foreach ( $tree as $property => $value ) {
			$new_key = $prefix . str_replace(
				'/',
				'-',
				strtolower( _wp_to_kebab_case( $property ) )
			);

			if ( is_array( $value ) ) {
				$new_prefix        = $new_key . $token;
				$flattened_subtree = static::flatten_tree( $value, $new_prefix, $token );
				foreach ( $flattened_subtree as $subtree_key => $subtree_value ) {
					$result[ $subtree_key ] = $subtree_value;
				}
			} else {
				$result[ $new_key ] = $value;
			}
		}
		return $result;
	}

	/**
	 * Given a styles array, it extracts the style properties
	 * and adds them to the $declarations array following the format:
	 *
	 *     array(
	 *       'name'  => 'property_name',
	 *       'value' => 'property_value',
	 *     )
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added the `$settings` and `$properties` parameters.
	 * @since 6.1.0 Added `$theme_json`, `$selector`, and `$use_root_padding` parameters.
	 * @since 6.5.0 Output a `min-height: unset` rule when `aspect-ratio` is set.
	 * @since 6.6.0 Pass current theme JSON settings to wp_get_typography_font_size_value(), and process background properties.
	 * @since 6.7.0 `ref` resolution of background properties, and assigning custom default values.
	 *
	 * @param array   $styles Styles to process.
	 * @param array   $settings Theme settings.
	 * @param array   $properties Properties metadata.
	 * @param array   $theme_json Theme JSON array.
	 * @param string  $selector The style block selector.
	 * @param boolean $use_root_padding Whether to add custom properties at root level.
	 * @return array Returns the modified $declarations.
	 */
	protected static function compute_style_properties( $styles, $settings = array(), $properties = null, $theme_json = null, $selector = null, $use_root_padding = null ) {
		if ( empty( $styles ) ) {
			return array();
		}

		if ( null === $properties ) {
			$properties = static::PROPERTIES_METADATA;
		}
		$declarations             = array();
		$root_variable_duplicates = array();
		$root_style_length        = strlen( '--wp--style--root--' );

		foreach ( $properties as $css_property => $value_path ) {
			if ( ! is_array( $value_path ) ) {
				continue;
			}

			$is_root_style = str_starts_with( $css_property, '--wp--style--root--' );
			if ( $is_root_style && ( static::ROOT_BLOCK_SELECTOR !== $selector || ! $use_root_padding ) ) {
				continue;
			}

			$value = static::get_property_value( $styles, $value_path, $theme_json );

			/*
			 * Root-level padding styles don't currently support strings with CSS shorthand values.
			 * This may change: https://github.com/WordPress/gutenberg/issues/40132.
			 */
			if ( '--wp--style--root--padding' === $css_property && is_string( $value ) ) {
				continue;
			}

			if ( $is_root_style && $use_root_padding ) {
				$root_variable_duplicates[] = substr( $css_property, $root_style_length );
			}

			/*
			 * Processes background image styles.
			 * If the value is a URL, it will be converted to a CSS `url()` value.
			 * For uploaded image (images with a database ID), apply size and position defaults,
			 * equal to those applied in block supports in lib/background.php.
			 */
			if ( 'background-image' === $css_property && ! empty( $value ) ) {
				$background_styles = wp_style_engine_get_styles(
					array( 'background' => array( 'backgroundImage' => $value ) )
				);
				$value             = $background_styles['declarations'][ $css_property ];
			}
			if ( empty( $value ) && static::ROOT_BLOCK_SELECTOR !== $selector && ! empty( $styles['background']['backgroundImage']['id'] ) ) {
				if ( 'background-size' === $css_property ) {
					$value = 'cover';
				}
				// If the background size is set to `contain` and no position is set, set the position to `center`.
				if ( 'background-position' === $css_property ) {
					$background_size = $styles['background']['backgroundSize'] ?? null;
					$value           = 'contain' === $background_size ? '50% 50%' : null;
				}
			}

			// Skip if empty and not "0" or value represents array of longhand values.
			$has_missing_value = empty( $value ) && ! is_numeric( $value );
			if ( $has_missing_value || is_array( $value ) ) {
				continue;
			}

			/*
			 * Look up protected properties, keyed by value path.
			 * Skip protected properties that are explicitly set to `null`.
			 */
			$path_string = implode( '.', $value_path );
			if (
				isset( static::PROTECTED_PROPERTIES[ $path_string ] ) &&
				_wp_array_get( $settings, static::PROTECTED_PROPERTIES[ $path_string ], null ) === null
			) {
				continue;
			}

			// Calculates fluid typography rules where available.
			if ( 'font-size' === $css_property ) {
				/*
				 * wp_get_typography_font_size_value() will check
				 * if fluid typography has been activated and also
				 * whether the incoming value can be converted to a fluid value.
				 * Values that already have a clamp() function will not pass the test,
				 * and therefore the original $value will be returned.
				 * Pass the current theme_json settings to override any global settings.
				 */
				$value = wp_get_typography_font_size_value( array( 'size' => $value ), $settings );
			}

			if ( 'aspect-ratio' === $css_property ) {
				// For aspect ratio to work, other dimensions rules must be unset.
				// This ensures that a fixed height does not override the aspect ratio.
				$declarations[] = array(
					'name'  => 'min-height',
					'value' => 'unset',
				);
			}

			$declarations[] = array(
				'name'  => $css_property,
				'value' => $value,
			);
		}

		// If a variable value is added to the root, the corresponding property should be removed.
		foreach ( $root_variable_duplicates as $duplicate ) {
			$discard = array_search( $duplicate, array_column( $declarations, 'name' ), true );
			if ( is_numeric( $discard ) ) {
				array_splice( $declarations, $discard, 1 );
			}
		}

		return $declarations;
	}

	/**
	 * Returns the style property for the given path.
	 *
	 * It also converts references to a path to the value
	 * stored at that location, e.g.
	 * { "ref": "style.color.background" } => "#fff".
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Added support for values of array type, which are returned as is.
	 * @since 6.1.0 Added the `$theme_json` parameter.
	 * @since 6.3.0 It no longer converts the internal format "var:preset|color|secondary"
	 *              to the standard form "--wp--preset--color--secondary".
	 *              This is already done by the sanitize method,
	 *              so every property will be in the standard form.
	 * @since 6.7.0 Added support for background image refs.
	 *
	 * @param array $styles Styles subtree.
	 * @param array $path   Which property to process.
	 * @param array $theme_json Theme JSON array.
	 * @return string|array Style property value.
	 */
	protected static function get_property_value( $styles, $path, $theme_json = null ) {
		$value = _wp_array_get( $styles, $path, '' );

		if ( '' === $value || null === $value ) {
			// No need to process the value further.
			return '';
		}

		/*
		 * This converts references to a path to the value at that path
		 * where the value is an array with a "ref" key, pointing to a path.
		 * For example: { "ref": "style.color.background" } => "#fff".
		 * In the case of backgroundImage, if both a ref and a URL are present in the value,
		 * the URL takes precedence and the ref is ignored.
		 */
		if ( is_array( $value ) && isset( $value['ref'] ) ) {
			$value_path = explode( '.', $value['ref'] );
			$ref_value  = _wp_array_get( $theme_json, $value_path );
			// Background Image refs can refer to a string or an array containing a URL string.
			$ref_value_url = $ref_value['url'] ?? null;
			// Only use the ref value if we find anything.
			if ( ! empty( $ref_value ) && ( is_string( $ref_value ) || is_string( $ref_value_url ) ) ) {
				$value = $ref_value;
			}

			if ( is_array( $ref_value ) && isset( $ref_value['ref'] ) ) {
				$path_string      = json_encode( $path );
				$ref_value_string = json_encode( $ref_value );
				_doing_it_wrong(
					'get_property_value',
					sprintf(
						/* translators: 1: theme.json, 2: Value name, 3: Value path, 4: Another value name. */
						__( 'Your %1$s file uses a dynamic value (%2$s) for the path at %3$s. However, the value at %3$s is also a dynamic value (pointing to %4$s) and pointing to another dynamic value is not supported. Please update %3$s to point directly to %4$s.' ),
						'theme.json',
						$ref_value_string,
						$path_string,
						$ref_value['ref']
					),
					'6.1.0'
				);
			}
		}

		if ( is_array( $value ) ) {
			return $value;
		}

		return $value;
	}

	/**
	 * Builds metadata for the setting nodes, which returns in the form of:
	 *
	 *     [
	 *       [
	 *         'path'     => ['path', 'to', 'some', 'node' ],
	 *         'selector' => 'CSS selector for some node'
	 *       ],
	 *       [
	 *         'path'     => [ 'path', 'to', 'other', 'node' ],
	 *         'selector' => 'CSS selector for other node'
	 *       ],
	 *     ]
	 *
	 * @since 5.8.0
	 *
	 * @param array $theme_json The tree to extract setting nodes from.
	 * @param array $selectors  List of selectors per block.
	 * @return array An array of setting nodes metadata.
	 */
	protected static function get_setting_nodes( $theme_json, $selectors = array() ) {
		$nodes = array();
		if ( ! isset( $theme_json['settings'] ) ) {
			return $nodes;
		}

		// Top-level.
		$nodes[] = array(
			'path'     => array( 'settings' ),
			'selector' => static::ROOT_CSS_PROPERTIES_SELECTOR,
		);

		// Calculate paths for blocks.
		if ( ! isset( $theme_json['settings']['blocks'] ) ) {
			return $nodes;
		}

		foreach ( $theme_json['settings']['blocks'] as $name => $node ) {
			$selector = null;
			if ( isset( $selectors[ $name ]['selector'] ) ) {
				$selector = $selectors[ $name ]['selector'];
			}

			$nodes[] = array(
				'path'     => array( 'settings', 'blocks', $name ),
				'selector' => $selector,
			);
		}

		return $nodes;
	}

	/**
	 * Builds metadata for the style nodes, which returns in the form of:
	 *
	 *     [
	 *       [
	 *         'path'     => [ 'path', 'to', 'some', 'node' ],
	 *         'selector' => 'CSS selector for some node',
	 *         'duotone'  => 'CSS selector for duotone for some node'
	 *       ],
	 *       [
	 *         'path'     => ['path', 'to', 'other', 'node' ],
	 *         'selector' => 'CSS selector for other node',
	 *         'duotone'  => null
	 *       ],
	 *     ]
	 *
	 * @since 5.8.0
	 * @since 6.6.0 Added options array for modifying generated nodes.
	 *
	 * @param array $theme_json The tree to extract style nodes from.
	 * @param array $selectors  List of selectors per block.
	 * @param array $options {
	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
	 *
	 *     @type bool $include_block_style_variations Includes style nodes for block style variations. Default false.
	 * }
	 * @return array An array of style nodes metadata.
	 */
	protected static function get_style_nodes( $theme_json, $selectors = array(), $options = array() ) {
		$nodes = array();
		if ( ! isset( $theme_json['styles'] ) ) {
			return $nodes;
		}

		// Top-level.
		$nodes[] = array(
			'path'     => array( 'styles' ),
			'selector' => static::ROOT_BLOCK_SELECTOR,
		);

		if ( isset( $theme_json['styles']['elements'] ) ) {
			foreach ( self::ELEMENTS as $element => $selector ) {
				if ( ! isset( $theme_json['styles']['elements'][ $element ] ) ) {
					continue;
				}
				$nodes[] = array(
					'path'     => array( 'styles', 'elements', $element ),
					'selector' => static::ELEMENTS[ $element ],
				);

				// Handle any pseudo selectors for the element.
				if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
					foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {

						if ( isset( $theme_json['styles']['elements'][ $element ][ $pseudo_selector ] ) ) {
							$nodes[] = array(
								'path'     => array( 'styles', 'elements', $element ),
								'selector' => static::append_to_selector( static::ELEMENTS[ $element ], $pseudo_selector ),
							);
						}
					}
				}
			}
		}

		// Blocks.
		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
			return $nodes;
		}

		$block_nodes = static::get_block_nodes( $theme_json, $selectors, $options );
		foreach ( $block_nodes as $block_node ) {
			$nodes[] = $block_node;
		}

		/**
		 * Filters the list of style nodes with metadata.
		 *
		 * This allows for things like loading block CSS independently.
		 *
		 * @since 6.1.0
		 *
		 * @param array $nodes Style nodes with metadata.
		 */
		return apply_filters( 'wp_theme_json_get_style_nodes', $nodes );
	}

	/**
	 * A public helper to get the block nodes from a theme.json file.
	 *
	 * @since 6.1.0
	 *
	 * @return array The block nodes in theme.json.
	 */
	public function get_styles_block_nodes() {
		return static::get_block_nodes( $this->theme_json );
	}

	/**
	 * Returns a filtered declarations array if there is a separator block with only a background
	 * style defined in theme.json by adding a color attribute to reflect the changes in the front.
	 *
	 * @since 6.1.1
	 *
	 * @param array $declarations List of declarations.
	 * @return array $declarations List of declarations filtered.
	 */
	private static function update_separator_declarations( $declarations ) {
		$background_color     = '';
		$border_color_matches = false;
		$text_color_matches   = false;

		foreach ( $declarations as $declaration ) {
			if ( 'background-color' === $declaration['name'] && ! $background_color && isset( $declaration['value'] ) ) {
				$background_color = $declaration['value'];
			} elseif ( 'border-color' === $declaration['name'] ) {
				$border_color_matches = true;
			} elseif ( 'color' === $declaration['name'] ) {
				$text_color_matches = true;
			}

			if ( $background_color && $border_color_matches && $text_color_matches ) {
				break;
			}
		}

		if ( $background_color && ! $border_color_matches && ! $text_color_matches ) {
			$declarations[] = array(
				'name'  => 'color',
				'value' => $background_color,
			);
		}

		return $declarations;
	}

	/**
	 * An internal method to get the block nodes from a theme.json file.
	 *
	 * @since 6.1.0
	 * @since 6.3.0 Refactored and stabilized selectors API.
	 * @since 6.6.0 Added optional selectors and options for generating block nodes.
	 * @since 6.7.0 Added $include_node_paths_only option.
	 *
	 * @param array $theme_json The theme.json converted to an array.
	 * @param array $selectors  Optional list of selectors per block.
	 * @param array $options {
	 *     Optional. An array of options for now used for internal purposes only (may change without notice).
	 *
	 *     @type bool $include_block_style_variations Include nodes for block style variations. Default false.
	 *     @type bool $include_node_paths_only        Return only block nodes node paths. Default false.
	 * }
	 * @return array The block nodes in theme.json.
	 */
	private static function get_block_nodes( $theme_json, $selectors = array(), $options = array() ) {
		$nodes = array();

		if ( ! isset( $theme_json['styles']['blocks'] ) ) {
			return $nodes;
		}

		$include_variations      = $options['include_block_style_variations'] ?? false;
		$include_node_paths_only = $options['include_node_paths_only'] ?? false;

		// If only node paths are to be returned, skip selector assignment.
		if ( ! $include_node_paths_only ) {
			$selectors = empty( $selectors ) ? static::get_blocks_metadata() : $selectors;
		}

		foreach ( $theme_json['styles']['blocks'] as $name => $node ) {
			$node_path = array( 'styles', 'blocks', $name );
			if ( $include_node_paths_only ) {
				$variation_paths = array();
				if ( $include_variations && isset( $node['variations'] ) ) {
					foreach ( $node['variations'] as $variation => $variation_node ) {
						$variation_paths[] = array(
							'path' => array( 'styles', 'blocks', $name, 'variations', $variation ),
						);
					}
				}
				$node = array(
					'path' => $node_path,
				);
				if ( ! empty( $variation_paths ) ) {
					$node['variations'] = $variation_paths;
				}
				$nodes[] = $node;
			} else {
				$selector = null;
				if ( isset( $selectors[ $name ]['selector'] ) ) {
					$selector = $selectors[ $name ]['selector'];
				}

				$duotone_selector = null;
				if ( isset( $selectors[ $name ]['duotone'] ) ) {
					$duotone_selector = $selectors[ $name ]['duotone'];
				}

				$feature_selectors = null;
				if ( isset( $selectors[ $name ]['selectors'] ) ) {
					$feature_selectors = $selectors[ $name ]['selectors'];
				}

				$variation_selectors = array();
				if ( $include_variations && isset( $node['variations'] ) ) {
					foreach ( $node['variations'] as $variation => $node ) {
						$variation_selectors[] = array(
							'path'     => array( 'styles', 'blocks', $name, 'variations', $variation ),
							'selector' => $selectors[ $name ]['styleVariations'][ $variation ],
						);
					}
				}

				$nodes[] = array(
					'name'       => $name,
					'path'       => $node_path,
					'selector'   => $selector,
					'selectors'  => $feature_selectors,
					'duotone'    => $duotone_selector,
					'features'   => $feature_selectors,
					'variations' => $variation_selectors,
					'css'        => $selector,
				);
			}

			if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) {
				foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) {
					$node_path = array( 'styles', 'blocks', $name, 'elements', $element );

					if ( $include_node_paths_only ) {
						$nodes[] = array(
							'path' => $node_path,
						);
						continue;
					}

					$nodes[] = array(
						'path'     => $node_path,
						'selector' => $selectors[ $name ]['elements'][ $element ],
					);

					// Handle any pseudo selectors for the element.
					if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) {
						foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) {
							if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) {
								$node_path = array( 'styles', 'blocks', $name, 'elements', $element );

								$nodes[] = array(
									'path'     => $node_path,
									'selector' => static::append_to_selector( $selectors[ $name ]['elements'][ $element ], $pseudo_selector ),
								);
							}
						}
					}
				}
			}
		}

		return $nodes;
	}

	/**
	 * Gets the CSS rules for a particular block from theme.json.
	 *
	 * @since 6.1.0
	 * @since 6.6.0 Setting a min-height of HTML when root styles have a background gradient or image.
	 *              Updated general global styles specificity to 0-1-0.
	 *              Fixed custom CSS output in block style variations.
	 *
	 * @param array $block_metadata Metadata about the block to get styles for.
	 * @return string Styles for the block.
	 */
	public function get_styles_for_block( $block_metadata ) {
		$node                 = _wp_array_get( $this->theme_json, $block_metadata['path'], array() );
		$use_root_padding     = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];
		$selector             = $block_metadata['selector'];
		$settings             = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
		$feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node );
		$is_root_selector     = static::ROOT_BLOCK_SELECTOR === $selector;

		// If there are style variations, generate the declarations for them, including any feature selectors the block may have.
		$style_variation_declarations = array();
		$style_variation_custom_css   = array();
		if ( ! empty( $block_metadata['variations'] ) ) {
			foreach ( $block_metadata['variations'] as $style_variation ) {
				$style_variation_node           = _wp_array_get( $this->theme_json, $style_variation['path'], array() );
				$clean_style_variation_selector = trim( $style_variation['selector'] );

				// Generate any feature/subfeature style declarations for the current style variation.
				$variation_declarations = static::get_feature_declarations_for_node( $block_metadata, $style_variation_node );

				// Combine selectors with style variation's selector and add to overall style variation declarations.
				foreach ( $variation_declarations as $current_selector => $new_declarations ) {
					/*
					 * Clean up any whitespace between comma separated selectors.
					 * This prevents these spaces breaking compound selectors such as:
					 * - `.wp-block-list:not(.wp-block-list .wp-block-list)`
					 * - `.wp-block-image img, .wp-block-image.my-class img`
					 */
					$clean_current_selector = preg_replace( '/,\s+/', ',', $current_selector );
					$shortened_selector     = str_replace( $block_metadata['selector'], '', $clean_current_selector );

					// Prepend the variation selector to the current selector.
					$split_selectors    = explode( ',', $shortened_selector );
					$updated_selectors  = array_map(
						static function ( $split_selector ) use ( $clean_style_variation_selector ) {
							return $clean_style_variation_selector . $split_selector;
						},
						$split_selectors
					);
					$combined_selectors = implode( ',', $updated_selectors );

					// Add the new declarations to the overall results under the modified selector.
					$style_variation_declarations[ $combined_selectors ] = $new_declarations;
				}

				// Compute declarations for remaining styles not covered by feature level selectors.
				$style_variation_declarations[ $style_variation['selector'] ] = static::compute_style_properties( $style_variation_node, $settings, null, $this->theme_json );
				// Store custom CSS for the style variation.
				if ( isset( $style_variation_node['css'] ) ) {
					$style_variation_custom_css[ $style_variation['selector'] ] = $this->process_blocks_custom_css( $style_variation_node['css'], $style_variation['selector'] );
				}
			}
		}
		/*
		 * Get a reference to element name from path.
		 * $block_metadata['path'] = array( 'styles','elements','link' );
		 * Make sure that $block_metadata['path'] describes an element node, like [ 'styles', 'element', 'link' ].
		 * Skip non-element paths like just ['styles'].
		 */
		$is_processing_element = in_array( 'elements', $block_metadata['path'], true );

		$current_element = $is_processing_element ? $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ] : null;

		$element_pseudo_allowed = array();

		if ( isset( $current_element, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
			$element_pseudo_allowed = static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ];
		}

		/*
		 * Check for allowed pseudo classes (e.g. ":hover") from the $selector ("a:hover").
		 * This also resets the array keys.
		 */
		$pseudo_matches = array_values(
			array_filter(
				$element_pseudo_allowed,
				static function ( $pseudo_selector ) use ( $selector ) {
					/*
					 * Check if the pseudo selector is in the current selector,
					 * ensuring it is not followed by a dash (e.g., :focus should not match :focus-visible).
					 */
					return preg_match( '/' . preg_quote( $pseudo_selector, '/' ) . '(?!-)/', $selector ) === 1;
				}
			)
		);

		$pseudo_selector = isset( $pseudo_matches[0] ) ? $pseudo_matches[0] : null;

		/*
		 * If the current selector is a pseudo selector that's defined in the allow list for the current
		 * element then compute the style properties for it.
		 * Otherwise just compute the styles for the default selector as normal.
		 */
		if ( $pseudo_selector && isset( $node[ $pseudo_selector ] ) &&
			isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] )
			&& in_array( $pseudo_selector, static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ], true )
		) {
			$declarations = static::compute_style_properties( $node[ $pseudo_selector ], $settings, null, $this->theme_json, $selector, $use_root_padding );
		} else {
			$declarations = static::compute_style_properties( $node, $settings, null, $this->theme_json, $selector, $use_root_padding );
		}

		$block_rules = '';

		/*
		 * 1. Bespoke declaration modifiers:
		 * - 'filter': Separate the declarations that use the general selector
		 * from the ones using the duotone selector.
		 * - 'background|background-image': set the html min-height to 100%
		 * to ensure the background covers the entire viewport.
		 */
		$declarations_duotone       = array();
		$should_set_root_min_height = false;

		foreach ( $declarations as $index => $declaration ) {
			if ( 'filter' === $declaration['name'] ) {
				/*
				 * 'unset' filters happen when a filter is unset
				 * in the site-editor UI. Because the 'unset' value
				 * in the user origin overrides the value in the
				 * theme origin, we can skip rendering anything
				 * here as no filter needs to be applied anymore.
				 * So only add declarations to with values other
				 * than 'unset'.
				 */
				if ( 'unset' !== $declaration['value'] ) {
					$declarations_duotone[] = $declaration;
				}
				unset( $declarations[ $index ] );
			}

			if ( $is_root_selector && ( 'background-image' === $declaration['name'] || 'background' === $declaration['name'] ) ) {
				$should_set_root_min_height = true;
			}
		}

		/*
		 * If root styles has a background-image or a background (gradient) set,
		 * set the min-height to '100%'. Minus `--wp-admin--admin-bar--height` for logged-in view.
		 * Setting the CSS rule on the HTML tag ensures background gradients and images behave similarly,
		 * and matches the behavior of the site editor.
		 */
		if ( $should_set_root_min_height ) {
			$block_rules .= static::to_ruleset(
				'html',
				array(
					array(
						'name'  => 'min-height',
						'value' => 'calc(100% - var(--wp-admin--admin-bar--height, 0px))',
					),
				)
			);
		}

		// Update declarations if there are separators with only background color defined.
		if ( '.wp-block-separator' === $selector ) {
			$declarations = static::update_separator_declarations( $declarations );
		}

		/*
		 * Root selector (body) styles should not be wrapped in `:root where()` to keep
		 * specificity at (0,0,1) and maintain backwards compatibility.
		 *
		 * Top-level element styles using element-only specificity selectors should
		 * not get wrapped in `:root :where()` to maintain backwards compatibility.
		 *
		 * Pseudo classes, e.g. :hover, :focus etc., are a class-level selector so
		 * still need to be wrapped in `:root :where` to cap specificity for nested
		 * variations etc. Pseudo selectors won't match the ELEMENTS selector exactly.
		 */
		$element_only_selector = $is_root_selector || (
			$current_element &&
			isset( static::ELEMENTS[ $current_element ] ) &&
			// buttons, captions etc. still need `:root :where()` as they are class based selectors.
			! isset( static::__EXPERIMENTAL_ELEMENT_CLASS_NAMES[ $current_element ] ) &&
			static::ELEMENTS[ $current_element ] === $selector
		);

		// 2. Generate and append the rules that use the general selector.
		$general_selector = $element_only_selector ? $selector : ":root :where($selector)";
		$block_rules     .= static::to_ruleset( $general_selector, $declarations );

		// 3. Generate and append the rules that use the duotone selector.
		if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) {
			$block_rules .= static::to_ruleset( $block_metadata['duotone'], $declarations_duotone );
		}

		// 4. Generate Layout block gap styles.
		if (
			! $is_root_selector &&
			! empty( $block_metadata['name'] )
		) {
			$block_rules .= $this->get_layout_styles( $block_metadata );
		}

		// 5. Generate and append the feature level rulesets.
		foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) {
			$block_rules .= static::to_ruleset( ":root :where($feature_selector)", $individual_feature_declarations );
		}

		// 6. Generate and append the style variation rulesets.
		foreach ( $style_variation_declarations as $style_variation_selector => $individual_style_variation_declarations ) {
			$block_rules .= static::to_ruleset( ":root :where($style_variation_selector)", $individual_style_variation_declarations );
			if ( isset( $style_variation_custom_css[ $style_variation_selector ] ) ) {
				$block_rules .= $style_variation_custom_css[ $style_variation_selector ];
			}
		}

		// 7. Generate and append any custom CSS rules.
		if ( isset( $node['css'] ) && ! $is_root_selector ) {
			$block_rules .= $this->process_blocks_custom_css( $node['css'], $selector );
		}

		return $block_rules;
	}

	/**
	 * Outputs the CSS for layout rules on the root.
	 *
	 * @since 6.1.0
	 * @since 6.6.0 Use `ROOT_CSS_PROPERTIES_SELECTOR` for CSS custom properties and improved consistency of root padding rules.
	 *              Updated specificity of body margin reset and first/last child selectors.
	 *
	 * @param string $selector The root node selector.
	 * @param array  $block_metadata The metadata for the root block.
	 * @return string The additional root rules CSS.
	 */
	public function get_root_layout_rules( $selector, $block_metadata ) {
		$css              = '';
		$settings         = isset( $this->theme_json['settings'] ) ? $this->theme_json['settings'] : array();
		$use_root_padding = isset( $this->theme_json['settings']['useRootPaddingAwareAlignments'] ) && true === $this->theme_json['settings']['useRootPaddingAwareAlignments'];

		/*
		 * If there are content and wide widths in theme.json, output them
		 * as custom properties on the body element so all blocks can use them.
		 */
		if ( isset( $settings['layout']['contentSize'] ) || isset( $settings['layout']['wideSize'] ) ) {
			$content_size = isset( $settings['layout']['contentSize'] ) ? $settings['layout']['contentSize'] : $settings['layout']['wideSize'];
			$content_size = static::is_safe_css_declaration( 'max-width', $content_size ) ? $content_size : 'initial';
			$wide_size    = isset( $settings['layout']['wideSize'] ) ? $settings['layout']['wideSize'] : $settings['layout']['contentSize'];
			$wide_size    = static::is_safe_css_declaration( 'max-width', $wide_size ) ? $wide_size : 'initial';
			$css         .= static::ROOT_CSS_PROPERTIES_SELECTOR . ' { --wp--style--global--content-size: ' . $content_size . ';';
			$css         .= '--wp--style--global--wide-size: ' . $wide_size . '; }';
		}

		/*
		 * Reset default browser margin on the body element.
		 * This is set on the body selector **before** generating the ruleset
		 * from the `theme.json`. This is to ensure that if the `theme.json` declares
		 * `margin` in its `spacing` declaration for the `body` element then these
		 * user-generated values take precedence in the CSS cascade.
		 * @link https://github.com/WordPress/gutenberg/issues/36147.
		 */
		$css .= ':where(body) { margin: 0; }';

		if ( $use_root_padding ) {
			// Top and bottom padding are applied to the outer block container.
			$css .= '.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }';
			// Right and left padding are applied to the first container with `.has-global-padding` class.
			$css .= '.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }';
			// Alignfull children of the container with left and right padding have negative margins so they can still be full width.
			$css .= '.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }';
			// Nested children of the container with left and right padding that are not full aligned do not get padding, unless they are direct children of an alignfull flow container.
			$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }';
			// Alignfull direct children of the containers that are targeted by the rule above do not need negative margins.
			$css .= '.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }';
		}

		$css .= '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }';
		$css .= '.wp-site-blocks > .alignright { float: right; margin-left: 2em; }';
		$css .= '.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }';

		// Block gap styles will be output unless explicitly set to `null`. See static::PROTECTED_PROPERTIES.
		if ( isset( $this->theme_json['settings']['spacing']['blockGap'] ) ) {
			$block_gap_value = static::get_property_value( $this->theme_json, array( 'styles', 'spacing', 'blockGap' ) );
			$css            .= ":where(.wp-site-blocks) > * { margin-block-start: $block_gap_value; margin-block-end: 0; }";
			$css            .= ':where(.wp-site-blocks) > :first-child { margin-block-start: 0; }';
			$css            .= ':where(.wp-site-blocks) > :last-child { margin-block-end: 0; }';

			// For backwards compatibility, ensure the legacy block gap CSS variable is still available.
			$css .= static::ROOT_CSS_PROPERTIES_SELECTOR . " { --wp--style--block-gap: $block_gap_value; }";
		}
		$css .= $this->get_layout_styles( $block_metadata );

		return $css;
	}

	/**
	 * For metadata values that can either be booleans or paths to booleans, gets the value.
	 *
	 *     $data = array(
	 *       'color' => array(
	 *         'defaultPalette' => true
	 *       )
	 *     );
	 *
	 *     static::get_metadata_boolean( $data, false );
	 *     // => false
	 *
	 *     static::get_metadata_boolean( $data, array( 'color', 'defaultPalette' ) );
	 *     // => true
	 *
	 * @since 6.0.0
	 *
	 * @param array      $data          The data to inspect.
	 * @param bool|array $path          Boolean or path to a boolean.
	 * @param bool       $default_value Default value if the referenced path is missing.
	 *                                  Default false.
	 * @return bool Value of boolean metadata.
	 */
	protected static function get_metadata_boolean( $data, $path, $default_value = false ) {
		if ( is_bool( $path ) ) {
			return $path;
		}

		if ( is_array( $path ) ) {
			$value = _wp_array_get( $data, $path );
			if ( null !== $value ) {
				return $value;
			}
		}

		return $default_value;
	}

	/**
	 * Merges new incoming data.
	 *
	 * @since 5.8.0
	 * @since 5.9.0 Duotone preset also has origins.
	 * @since 6.7.0 Replace background image objects during merge.
	 *
	 * @param WP_Theme_JSON $incoming Data to merge.
	 */
	public function merge( $incoming ) {
		$incoming_data    = $incoming->get_raw_data();
		$this->theme_json = array_replace_recursive( $this->theme_json, $incoming_data );

		/*
		 * Recompute all the spacing sizes based on the new hierarchy of data. In the constructor
		 * spacingScale and spacingSizes are both keyed by origin and VALID_ORIGINS is ordered, so
		 * we can allow partial spacingScale data to inherit missing data from earlier layers when
		 * computing the spacing sizes.
		 *
		 * This happens before the presets are merged to ensure that default spacing sizes can be
		 * removed from the theme origin if $prevent_override is true.
		 */
		$flattened_spacing_scale = array();
		foreach ( static::VALID_ORIGINS as $origin ) {
			$scale_path = array( 'settings', 'spacing', 'spacingScale', $origin );

			// Apply the base spacing scale to the current layer.
			$base_spacing_scale      = _wp_array_get( $this->theme_json, $scale_path, array() );
			$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $base_spacing_scale );

			$spacing_scale = _wp_array_get( $incoming_data, $scale_path, null );
			if ( ! isset( $spacing_scale ) ) {
				continue;
			}

			// Allow partial scale settings by merging with lower layers.
			$flattened_spacing_scale = array_replace( $flattened_spacing_scale, $spacing_scale );

			// Generate and merge the scales for this layer.
			$sizes_path           = array( 'settings', 'spacing', 'spacingSizes', $origin );
			$spacing_sizes        = _wp_array_get( $incoming_data, $sizes_path, array() );
			$spacing_scale_sizes  = static::compute_spacing_sizes( $flattened_spacing_scale );
			$merged_spacing_sizes = static::merge_spacing_sizes( $spacing_scale_sizes, $spacing_sizes );

			_wp_array_set( $incoming_data, $sizes_path, $merged_spacing_sizes );
		}

		/*
		 * The array_replace_recursive algorithm merges at the leaf level,
		 * but we don't want leaf arrays to be merged, so we overwrite it.
		 *
		 * For leaf values that are sequential arrays it will use the numeric indexes for replacement.
		 * We rather replace the existing with the incoming value, if it exists.
		 * This is the case of spacing.units.
		 *
		 * For leaf values that are associative arrays it will merge them as expected.
		 * This is also not the behavior we want for the current associative arrays (presets).
		 * We rather replace the existing with the incoming value, if it exists.
		 * This happens, for example, when we merge data from theme.json upon existing
		 * theme supports or when we merge anything coming from the same source twice.
		 * This is the case of color.palette, color.gradients, color.duotone,
		 * typography.fontSizes, or typography.fontFamilies.
		 *
		 * Additionally, for some preset types, we also want to make sure the
		 * values they introduce don't conflict with default values. We do so
		 * by checking the incoming slugs for theme presets and compare them
		 * with the equivalent default presets: if a slug is present as a default
		 * we remove it from the theme presets.
		 */
		$nodes        = static::get_setting_nodes( $incoming_data );
		$slugs_global = static::get_default_slugs( $this->theme_json, array( 'settings' ) );
		foreach ( $nodes as $node ) {
			// Replace the spacing.units.
			$path   = $node['path'];
			$path[] = 'spacing';
			$path[] = 'units';

			$content = _wp_array_get( $incoming_data, $path, null );
			if ( isset( $content ) ) {
				_wp_array_set( $this->theme_json, $path, $content );
			}

			// Replace the presets.
			foreach ( static::PRESETS_METADATA as $preset_metadata ) {
				$prevent_override = $preset_metadata['prevent_override'];
				if ( is_array( $prevent_override ) ) {
					$global_path  = array_merge( array( 'settings' ), $prevent_override );
					$global_value = _wp_array_get( $this->theme_json, $global_path, null );

					$node_level_path  = array_merge( $node['path'], $prevent_override );
					$prevent_override = _wp_array_get( $this->theme_json, $node_level_path, $global_value );
				}

				foreach ( static::VALID_ORIGINS as $origin ) {
					$base_path = $node['path'];
					foreach ( $preset_metadata['path'] as $leaf ) {
						$base_path[] = $leaf;
					}

					$path   = $base_path;
					$path[] = $origin;

					$content = _wp_array_get( $incoming_data, $path, null );
					if ( ! isset( $content ) ) {
						continue;
					}

					// Set names for theme presets based on the slug if they are not set and can use default names.
					if ( 'theme' === $origin && $preset_metadata['use_default_names'] ) {
						foreach ( $content as $key => $item ) {
							if ( ! isset( $item['name'] ) ) {
								$name = static::get_name_from_defaults( $item['slug'], $base_path );
								if ( null !== $name ) {
									$content[ $key ]['name'] = $name;
								}
							}
						}
					}

					// Filter out default slugs from theme presets when defaults should not be overridden.
					if ( 'theme' === $origin && $prevent_override ) {
						$slugs_node    = static::get_default_slugs( $this->theme_json, $node['path'] );
						$preset_global = _wp_array_get( $slugs_global, $preset_metadata['path'], array() );
						$preset_node   = _wp_array_get( $slugs_node, $preset_metadata['path'], array() );
						$preset_slugs  = array_merge_recursive( $preset_global, $preset_node );

						$content = static::filter_slugs( $content, $preset_slugs );
					}

					_wp_array_set( $this->theme_json, $path, $content );
				}
			}
		}

		/*
		 * Style values are merged at the leaf level, however
		 * some values provide exceptions, namely style values that are
		 * objects and represent unique definitions for the style.
		 */
		$style_nodes = static::get_block_nodes(
			$this->theme_json,
			array(),
			array( 'include_node_paths_only' => true )
		);

		// Add top-level styles.
		$style_nodes[] = array( 'path' => array( 'styles' ) );

		foreach ( $style_nodes as $style_node ) {
			$path = $style_node['path'];
			/*
			 * Background image styles should be replaced, not merged,
			 * as they themselves are specific object definitions for the style.
			 */
			$background_image_path = array_merge( $path, static::PROPERTIES_METADATA['background-image'] );
			$content               = _wp_array_get( $incoming_data, $background_image_path, null );
			if ( isset( $content ) ) {
				_wp_array_set( $this->theme_json, $background_image_path, $content );
			}
		}
	}

	/**
	 * Converts all filter (duotone) presets into SVGs.
	 *
	 * @since 5.9.1
	 *
	 * @param array $origins List of origins to process.
	 * @return string SVG filters.
	 */
	public function get_svg_filters( $origins ) {
		$blocks_metadata = static::get_blocks_metadata();
		$setting_nodes   = static::get_setting_nodes( $this->theme_json, $blocks_metadata );

		$filters = '';
		foreach ( $setting_nodes as $metadata ) {
			$node = _wp_array_get( $this->theme_json, $metadata['path'], array() );
			if ( empty( $node['color']['duotone'] ) ) {
				continue;
			}

			$duotone_presets = $node['color']['duotone'];

			foreach ( $origins as $origin ) {
				if ( ! isset( $duotone_presets[ $origin ] ) ) {
					continue;
				}
				foreach ( $duotone_presets[ $origin ] as $duotone_preset ) {
					$filters .= WP_Duotone::get_filter_svg_from_preset( $duotone_preset );
				}
			}
		}

		return $filters;
	}

	/**
	 * Determines whether a presets should be overridden or not.
	 *
	 * @since 5.9.0
	 * @deprecated 6.0.0 Use {@see 'get_metadata_boolean'} instead.
	 *
	 * @param array      $theme_json The theme.json like structure to inspect.
	 * @param array      $path       Path to inspect.
	 * @param bool|array $override   Data to compute whether to override the preset.
	 * @return bool
	 */
	protected static function should_override_preset( $theme_json, $path, $override ) {
		_deprecated_function( __METHOD__, '6.0.0', 'get_metadata_boolean' );

		if ( is_bool( $override ) ) {
			return $override;
		}

		/*
		 * The relationship between whether to override the defaults
		 * and whether the defaults are enabled is inverse:
		 *
		 * - If defaults are enabled  => theme presets should not be overridden
		 * - If defaults are disabled => theme presets should be overridden
		 *
		 * For example, a theme sets defaultPalette to false,
		 * making the default palette hidden from the user.
		 * In that case, we want all the theme presets to be present,
		 * so they should override the defaults.
		 */
		if ( is_array( $override ) ) {
			$value = _wp_array_get( $theme_json, array_merge( $path, $override ) );
			if ( isset( $value ) ) {
				return ! $value;
			}

			// Search the top-level key if none was found for this node.
			$value = _wp_array_get( $theme_json, array_merge( array( 'settings' ), $override ) );
			if ( isset( $value ) ) {
				return ! $value;
			}

			return true;
		}
	}

	/**
	 * Returns the default slugs for all the presets in an associative array
	 * whose keys are the preset paths and the leaves is the list of slugs.
	 *
	 * For example:
	 *
	 *     array(
	 *       'color' => array(
	 *         'palette'   => array( 'slug-1', 'slug-2' ),
	 *         'gradients' => array( 'slug-3', 'slug-4' ),
	 *       ),
	 *     )
	 *
	 * @since 5.9.0
	 *
	 * @param array $data      A theme.json like structure.
	 * @param array $node_path The path to inspect. It's 'settings' by default.
	 * @return array
	 */
	protected static function get_default_slugs( $data, $node_path ) {
		$slugs = array();

		foreach ( static::PRESETS_METADATA as $metadata ) {
			$path = $node_path;
			foreach ( $metadata['path'] as $leaf ) {
				$path[] = $leaf;
			}
			$path[] = 'default';

			$preset = _wp_array_get( $data, $path, null );
			if ( ! isset( $preset ) ) {
				continue;
			}

			$slugs_for_preset = array();
			foreach ( $preset as $item ) {
				if ( isset( $item['slug'] ) ) {
					$slugs_for_preset[] = $item['slug'];
				}
			}

			_wp_array_set( $slugs, $metadata['path'], $slugs_for_preset );
		}

		return $slugs;
	}

	/**
	 * Gets a `default`'s preset name by a provided slug.
	 *
	 * @since 5.9.0
	 *
	 * @param string $slug The slug we want to find a match from default presets.
	 * @param array  $base_path The path to inspect. It's 'settings' by default.
	 * @return string|null
	 */
	protected function get_name_from_defaults( $slug, $base_path ) {
		$path            = $base_path;
		$path[]          = 'default';
		$default_content = _wp_array_get( $this->theme_json, $path, null );
		if ( ! $default_content ) {
			return null;
		}
		foreach ( $default_content as $item ) {
			if ( $slug === $item['slug'] ) {
				return $item['name'];
			}
		}
		return null;
	}

	/**
	 * Removes the preset values whose slug is equal to any of given slugs.
	 *
	 * @since 5.9.0
	 *
	 * @param array $node  The node with the presets to validate.
	 * @param array $slugs The slugs that should not be overridden.
	 * @return array The new node.
	 */
	protected static function filter_slugs( $node, $slugs ) {
		if ( empty( $slugs ) ) {
			return $node;
		}

		$new_node = array();
		foreach ( $node as $value ) {
			if ( isset( $value['slug'] ) && ! in_array( $value['slug'], $slugs, true ) ) {
				$new_node[] = $value;
			}
		}

		return $new_node;
	}

	/**
	 * Removes insecure data from theme.json.
	 *
	 * @since 5.9.0
	 * @since 6.3.2 Preserves global styles block variations when securing styles.
	 * @since 6.6.0 Updated to allow variation element styles and $origin parameter.
	 *
	 * @param array  $theme_json Structure to sanitize.
	 * @param string $origin     Optional. What source of data this object represents.
	 *                           One of 'blocks', 'default', 'theme', or 'custom'. Default 'theme'.
	 * @return array Sanitized structure.
	 */
	public static function remove_insecure_properties( $theme_json, $origin = 'theme' ) {
		if ( ! in_array( $origin, static::VALID_ORIGINS, true ) ) {
			$origin = 'theme';
		}

		$sanitized = array();

		$theme_json = WP_Theme_JSON_Schema::migrate( $theme_json, $origin );

		$blocks_metadata     = static::get_blocks_metadata();
		$valid_block_names   = array_keys( $blocks_metadata );
		$valid_element_names = array_keys( static::ELEMENTS );
		$valid_variations    = static::get_valid_block_style_variations( $blocks_metadata );

		$theme_json = static::sanitize( $theme_json, $valid_block_names, $valid_element_names, $valid_variations );

		$blocks_metadata = static::get_blocks_metadata();
		$style_options   = array( 'include_block_style_variations' => true ); // Allow variations data.
		$style_nodes     = static::get_style_nodes( $theme_json, $blocks_metadata, $style_options );

		foreach ( $style_nodes as $metadata ) {
			$input = _wp_array_get( $theme_json, $metadata['path'], array() );
			if ( empty( $input ) ) {
				continue;
			}

			// The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability.
			if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) {
				$output = $input;
			} else {
				$output = static::remove_insecure_styles( $input );
			}

			/*
			 * Get a reference to element name from path.
			 * $metadata['path'] = array( 'styles', 'elements', 'link' );
			 */
			$current_element = $metadata['path'][ count( $metadata['path'] ) - 1 ];

			/*
			 * $output is stripped of pseudo selectors. Re-add and process them
			 * or insecure styles here.
			 */
			if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] ) ) {
				foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $current_element ] as $pseudo_selector ) {
					if (