Always an array; empty when the input has no usable shape. */ public static function shape( $value, int $depth = 0 ): array { if ( ! is_array( $value ) || $depth > self::MAX_DEPTH ) { return array(); } $out = array(); foreach ( $value as $key => $item ) { if ( count( $out ) >= self::MAX_ITEMS ) { break; } if ( is_string( $key ) && 1 !== preg_match( '/^[a-z0-9_\-]{1,' . self::MAX_KEY . '}$/D', $key ) ) { continue; } if ( is_array( $item ) ) { $out[ $key ] = self::shape( $item, $depth + 1 ); } elseif ( is_string( $item ) ) { $out[ $key ] = $item; } } return $out; } /** * Accepts a value only if it is a plain lower-case identifier (a post type or taxonomy name). * * This validates; it does not repair. WordPress's sanitize_key() would turn "../post" into "post" and * "POST" into "post"; here such a value is refused, so a malformed identifier can never become a valid * one. (Real post type and taxonomy names are already lower case, 20 and 32 characters at most.) * * @param mixed $value Candidate. * @return string The value, or an empty string when it is not an identifier. */ public static function identifier( $value ): string { return ( is_string( $value ) && 1 === preg_match( '/^[a-z0-9_\-]{1,32}$/D', $value ) ) ? $value : ''; } /** * Identifier check for a list. Entries that are not identifiers become empty strings (they are kept, so * the caller can see that something invalid was sent and refuse the request). * * @param mixed $value Candidate list, already text-sanitized. * @param int $max Most entries to look at. * @return string[] */ public static function identifiers( $value, int $max ): array { $out = array(); foreach ( array_slice( self::shape( $value ), 0, $max ) as $item ) { $out[] = self::identifier( $item ); } return $out; } }