Files
wp-asciiplayer/includes/options.php
LamGC f04600c37b Refresh player with native block editor preview and local assets
Preserve shortcode rendering, normalize options, bundle the player locally,
and add reproducible packaging and WordPress integration tests.
2026-10-12 17:56:08 +08:00

298 lines
8.7 KiB
PHP

<?php
/**
* Validation shared by the shortcode and dynamic block.
*
* @package LamGC\Asciiplayer
*/
namespace LamGC\Asciiplayer;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Normalize shortcode's lowercase names, camelCase block names and old aliases.
* Unknown options are intentionally discarded before reaching the player.
*
* @param mixed $attributes Input attributes.
* @return array
*/
function normalize_attributes( $attributes ) {
if ( ! is_array( $attributes ) ) {
return array();
}
$aliases = array(
'source' => 'source',
'src' => 'source',
'autoplay' => 'autoPlay',
'preload' => 'preload',
'loop' => 'loop',
'controls' => 'controls',
'fit' => 'fit',
'theme' => 'theme',
'speed' => 'speed',
'startat' => 'startAt',
'idletimelimit' => 'idleTimeLimit',
'cols' => 'cols',
'rows' => 'rows',
'fontsize' => 'terminalFontSize',
'terminalfontsize' => 'terminalFontSize',
'fontfamily' => 'terminalFontFamily',
'terminalfontfamily' => 'terminalFontFamily',
'lineheight' => 'terminalLineHeight',
'terminallineheight' => 'terminalLineHeight',
'poster' => 'poster',
'width' => 'width',
'style' => 'legacyStyle',
'legacystyle' => 'legacyStyle',
);
$result = array();
foreach ( $attributes as $key => $value ) {
if ( ! is_string( $key ) || ! is_scalar( $value ) ) {
continue;
}
$key = strtolower( str_replace( array( '-', '_' ), '', $key ) );
if ( isset( $aliases[ $key ] ) ) {
$result[ $aliases[ $key ] ] = $value;
}
}
return $result;
}
/**
* Accept HTTP(S) and site-root-relative recordings, never executable schemes.
* No requests are made by PHP, and URL credentials are never emitted.
*
* @param mixed $source Recording URL.
* @return string Sanitized URL, or an empty string for invalid input.
*/
function sanitize_source( $source ) {
if ( ! is_string( $source ) ) {
return '';
}
$source = trim( html_entity_decode( $source, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
if ( '' === $source || preg_match( '/[\x00-\x20\x7f<>"\\\\]/', $source ) ) {
return '';
}
$parts = wp_parse_url( $source );
if ( false === $parts || ! is_array( $parts ) || isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
return '';
}
if ( '/' === substr( $source, 0, 1 ) ) {
// Reject protocol-relative URLs, including slash/backslash variants.
if ( '/' === substr( $source, 1, 1 ) || isset( $parts['host'] ) || isset( $parts['scheme'] ) ) {
return '';
}
} elseif ( empty( $parts['host'] ) || empty( $parts['scheme'] ) || ! in_array( strtolower( $parts['scheme'] ), array( 'http', 'https' ), true ) ) {
return '';
}
return esc_url_raw( $source, array( 'http', 'https' ) );
}
/**
* Parse explicit false values rather than casting a nonempty string to true.
*
* @param mixed $value Input.
* @return bool|null Null indicates an unrecognized value.
*/
function parse_boolean( $value ) {
if ( is_bool( $value ) ) {
return $value;
}
if ( ! is_scalar( $value ) ) {
return null;
}
$value = strtolower( trim( (string) $value ) );
if ( in_array( $value, array( '1', 'true', 'yes', 'on' ), true ) ) {
return true;
}
if ( in_array( $value, array( '0', 'false', 'no', 'off' ), true ) ) {
return false;
}
return null;
}
/**
* Validate a finite number, retaining legitimate zero values.
*
* @param mixed $value Input.
* @param float $min Minimum value.
* @param float $max Maximum value.
* @return float|null
*/
function parse_number( $value, $min, $max ) {
if ( is_bool( $value ) || ! is_scalar( $value ) || ! is_numeric( $value ) ) {
return null;
}
$value = (float) $value;
return is_finite( $value ) && $value >= $min && $value <= $max ? $value : null;
}
/**
* Convert the player's supported timecode syntax to seconds.
*
* @param mixed $value Seconds, mm:ss or hh:mm:ss.
* @return float|null
*/
function parse_time( $value ) {
$number = parse_number( $value, 0, PHP_INT_MAX );
if ( null !== $number ) {
return $number;
}
if ( ! is_string( $value ) || ! preg_match( '/^\d+(?::[0-5]?\d){1,2}(?:\.\d+)?$/', trim( $value ) ) ) {
return null;
}
$total = 0.0;
foreach ( explode( ':', trim( $value ) ) as $part ) {
$total = $total * 60 + (float) $part;
}
return is_finite( $total ) && $total <= PHP_INT_MAX ? $total : null;
}
/**
* Return a single safe CSS length for width/font size.
*
* @param mixed $value Input.
* @param bool $font Whether player font-size names are allowed.
* @return string
*/
function sanitize_size( $value, $font = false ) {
if ( ! is_string( $value ) && ! is_numeric( $value ) ) {
return '';
}
$value = strtolower( trim( (string) $value ) );
if ( $font && in_array( $value, array( 'small', 'medium', 'big' ), true ) ) {
return $value;
}
return preg_match( '/^(?:0|(?:\d+(?:\.\d+)?|\.\d+)(?:px|%|em|rem|vw|vh|vmin|vmax|ch))$/', $value ) ? $value : '';
}
/**
* Produce only typed, supported player options; never JavaScript source.
*
* @param array $attributes Normalized attributes.
* @return array
*/
function player_options( array $attributes ) {
$options = array(
'autoPlay' => false,
'preload' => true,
'fit' => 'width',
);
foreach ( array( 'autoPlay', 'preload', 'loop' ) as $name ) {
if ( isset( $attributes[ $name ] ) ) {
$value = parse_boolean( $attributes[ $name ] );
if ( null !== $value ) {
$options[ $name ] = $value;
} elseif ( 'loop' === $name ) {
// Older shortcodes can specify a finite number of replays.
$value = parse_number( $attributes[ $name ], 1, 10000 );
if ( null !== $value && floor( $value ) === $value ) {
$options[ $name ] = (int) $value;
}
}
}
}
if ( isset( $attributes['controls'] ) ) {
$value = strtolower( trim( (string) $attributes['controls'] ) );
if ( 'auto' === $value ) {
$options['controls'] = 'auto';
} elseif ( in_array( $value, array( 'always', 'never' ), true ) ) {
$options['controls'] = 'always' === $value;
} else {
$value = parse_boolean( $attributes['controls'] );
if ( null !== $value ) {
$options['controls'] = $value;
}
}
}
if ( isset( $attributes['fit'] ) ) {
$value = strtolower( trim( (string) $attributes['fit'] ) );
if ( in_array( $value, array( 'width', 'height', 'both', 'none' ), true ) ) {
$options['fit'] = $value;
} elseif ( false === parse_boolean( $attributes['fit'] ) ) {
$options['fit'] = 'none';
}
}
if ( isset( $attributes['theme'] ) && preg_match( '/^(?:auto\/)?[a-zA-Z0-9_-]{1,64}$/', (string) $attributes['theme'] ) ) {
$options['theme'] = (string) $attributes['theme'];
}
foreach ( array( 'speed' => array( 0.01, 100 ), 'idleTimeLimit' => array( 0.001, PHP_INT_MAX ), 'terminalLineHeight' => array( 0.1, 10 ) ) as $name => $range ) {
if ( isset( $attributes[ $name ] ) ) {
$value = parse_number( $attributes[ $name ], $range[0], $range[1] );
if ( null !== $value ) {
$options[ $name ] = $value;
}
}
}
if ( isset( $attributes['startAt'] ) ) {
$value = parse_time( $attributes['startAt'] );
if ( null !== $value ) {
$options['startAt'] = $value;
}
}
foreach ( array( 'cols', 'rows' ) as $name ) {
if ( isset( $attributes[ $name ] ) ) {
$value = parse_number( $attributes[ $name ], 1, 1000 );
if ( null !== $value && floor( $value ) === $value ) {
$options[ $name ] = (int) $value;
}
}
}
if ( isset( $attributes['terminalFontSize'] ) ) {
$value = sanitize_size( $attributes['terminalFontSize'], true );
if ( '' !== $value ) {
$options['terminalFontSize'] = $value;
}
}
if ( isset( $attributes['terminalFontFamily'] ) ) {
$value = trim( (string) $attributes['terminalFontFamily'] );
if ( strlen( $value ) <= 300 && preg_match( '/^[a-zA-Z0-9 _.,\'"-]+$/', $value ) ) {
$options['terminalFontFamily'] = $value;
}
}
if ( isset( $attributes['poster'] ) ) {
$value = (string) $attributes['poster'];
if ( 0 === strpos( $value, 'npt:' ) && null !== parse_time( substr( $value, 4 ) ) ) {
$options['poster'] = $value;
} elseif ( 0 === strpos( $value, 'data:text/plain,' ) && strlen( $value ) <= 32768 ) {
$options['poster'] = $value;
}
}
return $options;
}
/**
* Filter legacy inline style, then apply the dedicated width control if set.
*
* @param array $attributes Normalized attributes.
* @return string
*/
function player_style( array $attributes ) {
$style = isset( $attributes['legacyStyle'] ) ? safecss_filter_attr( (string) $attributes['legacyStyle'] ) : '';
if ( isset( $attributes['width'] ) ) {
$width = sanitize_size( $attributes['width'] );
if ( '' !== $width ) {
$style = rtrim( $style, '; ' ) . ';width:' . $width . ';';
}
}
return ltrim( $style, '; ' );
}