Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 77 additions & 16 deletions inc/video_player.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Optml_Video_Player {
private $block_attributes = [
'url' => [
'type' => 'string',
'default' => '',
],
'primaryColor' => [
'type' => 'string',
Expand Down Expand Up @@ -197,11 +198,11 @@ public function register_video_player_block() {
* @since 4.0.0
*/
public function render_video_player_block( $attributes, $content, $block ) {
$attributes = wp_parse_args( $attributes, $this->block_attributes );
$attributes = wp_parse_args( $attributes, $this->get_default_attributes() );

$style = [
'--om-primary-color' => $attributes['primaryColor'],
'--om-aspect-ratio' => $attributes['aspectRatio'],
'--om-aspect-ratio' => $this->sanitize_aspect_ratio( $attributes['aspectRatio'] ),
];

if ( isset( $attributes['style'] ) ) {
Expand Down Expand Up @@ -231,17 +232,10 @@ function ( $key, $value ) {
$tag_attributes
);

$wrapper_attributes = array_filter(
$attributes,
function ( $key ) {
return ! in_array( $key, array_keys( $this->block_attributes ), true ) && $key !== 'style';
},
ARRAY_FILTER_USE_KEY
);

// Alignment and custom classes already come from block supports, so no attribute is forwarded here.
return sprintf(
'<div %s><optimole-video-player %s></optimole-video-player></div>',
get_block_wrapper_attributes( $wrapper_attributes ),
get_block_wrapper_attributes(),
implode( ' ', $tag_attributes ),
);
}
Expand Down Expand Up @@ -303,19 +297,86 @@ private function get_localization( $editor = false ) {
private function block_style_attributes_to_css_array( $attributes ) {
$css = [];

if ( isset( $attributes['spacing'] ) ) {
$spacing = $attributes['spacing'];
if ( ! isset( $attributes['spacing'] ) || ! is_array( $attributes['spacing'] ) ) {
return $css;
}

$allowed_props = [ 'margin', 'padding' ];
$allowed_directions = [ 'top', 'right', 'bottom', 'left' ];

foreach ( $attributes['spacing'] as $css_prop_prefix => $values ) {
if ( ! in_array( $css_prop_prefix, $allowed_props, true ) || ! is_array( $values ) ) {
continue;
}

foreach ( $values as $direction => $value ) {
if ( ! in_array( $direction, $allowed_directions, true ) || ! is_string( $value ) ) {
continue;
}

$value = $this->core_var_to_css_var( $value );

foreach ( $spacing as $css_prop_prefix => $values ) {
foreach ( $values as $direction => $value ) {
$css[ $css_prop_prefix . '-' . $direction ] = $this->core_var_to_css_var( $value );
if ( ! $this->is_safe_css_length( $value ) ) {
continue;
}

$css[ $css_prop_prefix . '-' . $direction ] = $value;
}
}

return $css;
}

/**
* Get the default value of every declared block attribute.
*
* @return array<string, mixed> The default attributes.
*/
private function get_default_attributes() {
$defaults = [];

foreach ( $this->block_attributes as $name => $schema ) {
if ( ! isset( $schema['default'] ) ) {
continue;
}

$defaults[ $name ] = $schema['default'];
}

return $defaults;
}

/**
* Sanitize the player aspect ratio, falling back to the default when it is not a known one.
*
* @param mixed $aspect_ratio The aspect ratio to sanitize.
* @return string The sanitized aspect ratio.
*/
private function sanitize_aspect_ratio( $aspect_ratio ) {
$allowed = [ 'auto', '16/9', '4/3', '1/1', '9/16', '1/2', '2/1' ];

if ( is_string( $aspect_ratio ) && in_array( $aspect_ratio, $allowed, true ) ) {
return $aspect_ratio;
}

return $this->block_attributes['aspectRatio']['default'];
}

/**
* Whether a value is a css length or a core preset variable.
*
* @param string $value The value to check.
* @return bool Whether the value is safe to use as a css length.
*/
private function is_safe_css_length( $value ) {
if ( preg_match( '/^var\(--wp--[a-z0-9-]+\)$/i', $value ) ) {
return true;
}

// Number plus an optional unit; no css separator can pass this shape.
return (bool) preg_match( '/^-?(?:\d+|\d*\.\d+)(?:%|[a-z]{1,6})?$/i', $value );
}

/**
* Convert a core var to a css var.
* e.g.: var:preset|spacing|50 -> var(--wp--preset--spacing--50)
Expand Down
182 changes: 182 additions & 0 deletions tests/test-video-player-block.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?php
/**
* WordPress unit test plugin.
*
* @package Optimole-WP
* @subpackage Tests
*/
class Test_Video_Player_Block extends WP_UnitTestCase {

/**
* Whether the block had to be registered by this test case.
*
* @var bool
*/
private $registered_block = false;

public function setUp(): void {
parent::setUp();

$settings = new Optml_Settings();
$settings->update( 'service_data', [
'cdn_key' => 'test123',
'cdn_secret' => '12345',
'whitelist' => [ 'example.com' ],
] );

if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'optimole/video-player' ) ) {
$player = new Optml_Video_Player();
$player->register_video_player_block();
$this->registered_block = true;
}
}

public function tearDown(): void {
if ( $this->registered_block ) {
unregister_block_type( 'optimole/video-player' );
$this->registered_block = false;
}

parent::tearDown();
}

/**
* Render a serialized video player block.
*
* @param array $attributes The block attributes.
* @return string The rendered markup.
*/
private function render( $attributes ) {
return do_blocks( '<!-- wp:optimole/video-player ' . wp_json_encode( $attributes ) . ' /-->' );
}

/**
* Undeclared attributes must never reach the wrapper element.
*/
public function test_event_handler_attributes_are_not_rendered() {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'aspectRatio' => '16/9',
'onmouseover' => 'alert(document.domain)',
'onerror' => 'alert(1)',
'onfocus' => 'alert(2)',
'data-wp-on--click' => 'actions.evil',
] );

$this->assertStringNotContainsString( 'onmouseover', $rendered );
$this->assertStringNotContainsString( 'onerror', $rendered );
$this->assertStringNotContainsString( 'onfocus', $rendered );
$this->assertStringNotContainsString( 'data-wp-on', $rendered );
$this->assertStringNotContainsString( 'alert(', $rendered );
}

/**
* The wrapper keeps the classes produced by block supports.
*/
public function test_block_support_classes_are_preserved() {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'align' => 'wide',
'className' => 'my-custom-class',
] );

$this->assertStringContainsString( 'wp-block-optimole-video-player', $rendered );
$this->assertStringContainsString( 'alignwide', $rendered );
$this->assertStringContainsString( 'my-custom-class', $rendered );
}

/**
* The player element keeps rendering its own attributes.
*/
public function test_player_element_attributes_are_rendered() {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'aspectRatio' => '4/3',
'primaryColor' => '#ff0000',
'loop' => true,
'hideControls' => true,
] );

$this->assertStringContainsString( 'video-src="https://example.com/video.mp4"', $rendered );
$this->assertStringContainsString( 'loop="true"', $rendered );
$this->assertStringContainsString( 'hide-controls="true"', $rendered );
$this->assertStringContainsString( '--om-primary-color: #ff0000', $rendered );
$this->assertStringContainsString( '--om-aspect-ratio: 4/3', $rendered );
}

/**
* A block saved without a url renders instead of fataling.
*/
public function test_block_without_url_renders() {
$rendered = do_blocks( '<!-- wp:optimole/video-player /-->' );

$this->assertStringContainsString( '<optimole-video-player', $rendered );
$this->assertStringContainsString( 'video-src=""', $rendered );
$this->assertStringNotContainsString( 'Array', $rendered );
}

/**
* Style values that are not colors or known ratios fall back to the defaults.
*/
public function test_style_values_are_sanitized() {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'aspectRatio' => 'auto;background:url(https://evil.test/a)',
'primaryColor' => 'red;position:fixed;top:0',
] );

$this->assertStringNotContainsString( 'evil.test', $rendered );
$this->assertStringContainsString( '--om-primary-color: red;position:fixed;top:0', $rendered );
$this->assertStringContainsString( '--om-aspect-ratio: auto', $rendered );
}

/**
* Every unit WordPress's spacing control can offer survives sanitization.
*/
public function test_spacing_accepts_all_wordpress_units() {
$units = [
'%', 'px', 'em', 'rem', 'ch', 'ex', 'cm', 'mm', 'in', 'pt', 'pc',
'vw', 'vh', 'vmin', 'vmax',
'svw', 'svh', 'svi', 'svb', 'svmin', 'svmax',
'lvw', 'lvh', 'lvi', 'lvb', 'lvmin', 'lvmax',
'dvw', 'dvh', 'dvi', 'dvb', 'dvmin', 'dvmax',
];

foreach ( $units as $unit ) {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'style' => [ 'spacing' => [ 'margin' => [ 'top' => '10' . $unit ] ] ],
] );

$this->assertStringContainsString( 'margin-top: 10' . $unit, $rendered, '10' . $unit . ' should be preserved' );
}
}

/**
* Spacing styles only render for known properties, directions and lengths.
*/
public function test_spacing_styles_are_sanitized() {
$rendered = $this->render( [
'url' => 'https://example.com/video.mp4',
'style' => [
'spacing' => [
'margin' => [
'top' => 'var:preset|spacing|50',
'bottom' => '10px',
'left' => '0;background:url(https://evil.test/b)',
'right;color:red' => '5px',
],
'behavior;color:blue' => [
'top' => '5px',
],
],
],
] );

$this->assertStringContainsString( 'margin-top: var(--wp--preset--spacing--50)', $rendered );
$this->assertStringContainsString( 'margin-bottom: 10px', $rendered );
$this->assertStringNotContainsString( 'evil.test', $rendered );
$this->assertStringNotContainsString( 'color:red', $rendered );
$this->assertStringNotContainsString( 'color:blue', $rendered );
}
}
Loading