$field Field definition. * @param mixed $value Raw value. * @return string|int Empty string when there is nothing valid; an integer for `image`. */ public static function clean( array $field, $value ) { if ( is_array( $value ) || is_object( $value ) ) { return ''; } $value = is_bool( $value ) ? '' : trim( (string) $value ); $max = isset( $field['max'] ) ? (int) $field['max'] : 0; switch ( (string) $field['kind'] ) { case 'text': return Sanitize::text( $value, $max ); case 'textarea': return self::textarea( $value, $max ); case 'lines': return self::lines( $value ); case 'url': return Sanitize::url( $value ); case 'date': return self::date( $value ); case 'datetime': return self::datetime( $value ); case 'integer': return self::integer( $value, $field ); case 'decimal': return self::decimal( $value ); case 'currency': return 1 === preg_match( '/^[A-Za-z]{3}$/', $value ) ? strtoupper( $value ) : ''; case 'country': return 1 === preg_match( '/^[A-Za-z]{2}$/', $value ) ? strtoupper( $value ) : ''; case 'gtin': $digits = (string) preg_replace( '/[\s-]+/', '', $value ); return 1 === preg_match( '/^(\d{8}|\d{12,14})$/', $digits ) ? $digits : ''; case 'select': return isset( $field['options'] ) && is_array( $field['options'] ) && array_key_exists( $value, $field['options'] ) ? $value : ''; case 'image': return is_numeric( $value ) ? max( 0, (int) $value ) : 0; } return ''; } /** * The lines of a `lines` value, cleaned. * * @since 1.0.0 * * @param string $value Stored value. * @return string[] */ public static function split_lines( string $value ): array { $lines = array(); $parts = preg_split( '/\r\n|\r|\n/', $value ); foreach ( is_array( $parts ) ? $parts : array() as $line ) { $line = Sanitize::text( $line, 500 ); if ( '' !== $line ) { $lines[] = $line; } if ( count( $lines ) >= self::MAX_LINES ) { break; } } return $lines; } /** * Minutes as an ISO 8601 duration, such as `PT1H30M`. * * @since 1.0.0 * * @param int $minutes Minutes. * @return string Empty for zero or less. */ public static function duration_from_minutes( int $minutes ): string { if ( $minutes < 1 ) { return ''; } $hours = intdiv( $minutes, 60 ); $rest = $minutes % 60; return 'PT' . ( $hours > 0 ? $hours . 'H' : '' ) . ( $rest > 0 ? $rest . 'M' : '' ); } /** * Seconds as an ISO 8601 duration, such as `PT1M30S`. * * @since 1.0.0 * * @param int $seconds Seconds. * @return string Empty for zero or less. */ public static function duration_from_seconds( int $seconds ): string { if ( $seconds < 1 ) { return ''; } $hours = intdiv( $seconds, 3600 ); $minutes = intdiv( $seconds % 3600, 60 ); $rest = $seconds % 60; return 'PT' . ( $hours > 0 ? $hours . 'H' : '' ) . ( $minutes > 0 ? $minutes . 'M' : '' ) . ( $rest > 0 ? $rest . 'S' : '' ); } /** * Plain text with line breaks kept. * * @since 1.0.0 * * @param string $value Raw value. * @param int $max Longest accepted value; 0 for no limit. * @return string */ private static function textarea( string $value, int $max ): string { $value = sanitize_textarea_field( $value ); if ( $max > 0 && mb_strlen( $value ) > $max ) { $value = mb_substr( $value, 0, $max ); } return $value; } /** * Lines of text, kept as one string with line breaks. * * @since 1.0.0 * * @param string $value Raw value. * @return string */ private static function lines( string $value ): string { return implode( "\n", self::split_lines( $value ) ); } /** * A calendar date. * * @since 1.0.0 * * @param string $value Raw value. * @return string `YYYY-MM-DD` or empty. */ private static function date( string $value ): string { $date = DateTimeImmutable::createFromFormat( '!Y-m-d', substr( $value, 0, 10 ) ); if ( false === $date || $date->format( 'Y-m-d' ) !== substr( $value, 0, 10 ) ) { return ''; } return $date->format( 'Y-m-d' ); } /** * A date and time as ISO 8601 with a time zone offset. * * @since 1.0.0 * * @param string $value Raw value, such as `2026-11-01T19:00`, `2026-11-01 19:00:00` or `2026-11-01T19:00:00+01:00`. * @return string */ private static function datetime( string $value ): string { if ( 1 !== preg_match( '/^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/', $value, $parts ) ) { return self::date( $value ); } if ( '' === self::date( $parts[1] ) || (int) $parts[2] > 23 || (int) $parts[3] > 59 || ( isset( $parts[4] ) && '' !== $parts[4] && (int) $parts[4] > 59 ) ) { return ''; } $time = $parts[2] . ':' . $parts[3] . ':' . ( isset( $parts[4] ) && '' !== $parts[4] ? $parts[4] : '00' ); $zone = isset( $parts[5] ) ? (string) $parts[5] : ''; if ( 'Z' === $zone ) { $zone = '+00:00'; } elseif ( '' === $zone ) { $zone = wp_timezone()->getOffset( new DateTimeImmutable( $parts[1] . 'T' . $time, wp_timezone() ) ); $sign = $zone < 0 ? '-' : '+'; $zone = $sign . gmdate( 'H:i', abs( (int) $zone ) ); } elseif ( false === strpos( $zone, ':' ) ) { $zone = substr( $zone, 0, 3 ) . ':' . substr( $zone, 3 ); } return $parts[1] . 'T' . $time . $zone; } /** * A whole number within the limits of the field. * * @since 1.0.0 * * @param string $value Raw value. * @param array $field Field definition (`min`, `max`). * @return string The number as text, or empty. */ private static function integer( string $value, array $field ): string { if ( 1 !== preg_match( '/^\d{1,9}$/', $value ) ) { return ''; } $number = (int) $value; if ( isset( $field['min'] ) && $number < (int) $field['min'] ) { return ''; } if ( isset( $field['max'] ) && $number > (int) $field['max'] ) { return ''; } return (string) $number; } /** * A decimal number with a dot, such as a price. * * @since 1.0.0 * * @param string $value Raw value. * @return string */ private static function decimal( string $value ): string { $value = str_replace( ',', '.', $value ); if ( 1 !== preg_match( '/^\d{1,9}(\.\d{1,4})?$/', $value ) ) { return ''; } return $value; } }