diff --git a/.github/workflows/build-test-measure.yml b/.github/workflows/build-test-measure.yml index 4349a3673f2..0b34c20be8f 100644 --- a/.github/workflows/build-test-measure.yml +++ b/.github/workflows/build-test-measure.yml @@ -214,6 +214,7 @@ jobs: # phpstan requires PHP 7.1+. php-version: 7.4 extensions: dom, iconv, json, libxml, zip + tools: phpstan - name: Get Composer Cache Directory id: composer-cache @@ -231,7 +232,9 @@ jobs: run: composer install - name: Static Analysis (PHPStan) - run: vendor/bin/phpstan analyse + run: | + phpstan --version + phpstan analyse #----------------------------------------------------------------------------------------------------------------------- diff --git a/includes/embeds/class-amp-base-embed-handler.php b/includes/embeds/class-amp-base-embed-handler.php index 70b869ff1d2..ace710a8000 100644 --- a/includes/embeds/class-amp-base-embed-handler.php +++ b/includes/embeds/class-amp-base-embed-handler.php @@ -21,7 +21,9 @@ abstract class AMP_Base_Embed_Handler { /** * Default width. * - * @var int + * In some cases, this may be the string 'auto' when a fixed-height layout is used. + * + * @var int|string */ protected $DEFAULT_WIDTH = 600; diff --git a/includes/embeds/class-amp-youtube-embed-handler.php b/includes/embeds/class-amp-youtube-embed-handler.php index 074d272773b..3319e589cc6 100644 --- a/includes/embeds/class-amp-youtube-embed-handler.php +++ b/includes/embeds/class-amp-youtube-embed-handler.php @@ -5,6 +5,13 @@ * @package AMP */ +use AmpProject\Attribute; +use AmpProject\Dom\Document; +use AmpProject\Dom\Element; +use AmpProject\Extension; +use AmpProject\Layout; +use AmpProject\Tag; + /** * Class AMP_YouTube_Embed_Handler * @@ -28,7 +35,7 @@ class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler { /** * Ratio for calculating the default height from the content width. * - * @param float + * @var float */ const RATIO = 0.5625; @@ -46,6 +53,24 @@ class AMP_YouTube_Embed_Handler extends AMP_Base_Embed_Handler { */ protected $DEFAULT_HEIGHT = 338; + /** + * List of domains that are applicable for this embed. + * + * @var string[] + */ + const APPLICABLE_DOMAINS = [ 'youtu.be', 'youtube.com', 'youtube-nocookie.com' ]; + + /** + * Attributes from iframe which are copied to amp-youtube. + * + * @var string[] + */ + const IFRAME_ATTRIBUTES = [ + Attribute::TITLE, + Attribute::HEIGHT, + Attribute::WIDTH, + ]; + /** * AMP_YouTube_Embed_Handler constructor. * @@ -75,6 +100,7 @@ public function register_embed() { */ public function unregister_embed() { remove_filter( 'embed_oembed_html', [ $this, 'filter_embed_oembed_html' ], 10 ); + remove_filter( 'wp_video_shortcode_override', [ $this, 'video_override' ], 10 ); } /** @@ -82,117 +108,274 @@ public function unregister_embed() { * * @param string $cache Cache for oEmbed. * @param string $url Embed URL. + * * @return string Embed. */ public function filter_embed_oembed_html( $cache, $url ) { - $id = $this->get_video_id_from_url( $url ); - if ( ! $id ) { + + if ( empty( $cache ) || empty( $url ) ) { return $cache; } - $props = $this->parse_props( $cache, $url, $id ); - if ( empty( $props ) ) { + $video_id = $this->get_video_id_from_url( $url ); + + if ( ! $video_id ) { return $cache; } - $props['video_id'] = $id; - return $this->render( $props, $url ); + return $this->render( $cache, $url, $video_id ); } /** - * Parse AMP component from iframe. + * Convert YouTube iframe into AMP YouTube component. * - * @param string $html HTML. - * @param string $url Embed URL, for fallback purposes. + * @param string $html HTML markup of YouTube iframe. + * @param string $url YouTube URL. * @param string $video_id YouTube video ID. - * @return array|null Props for rendering the component, or null if unable to parse. + * + * @return string HTML markup of AMP YouTube component. */ - private function parse_props( $html, $url, $video_id ) { - $props = $this->match_element_attributes( $html, 'iframe', [ 'title', 'height', 'width' ] ); - if ( ! isset( $props ) ) { - return null; - } + public function render( $html, $url, $video_id ) { - $img_attributes = [ - 'src' => esc_url_raw( sprintf( 'https://i.ytimg.com/vi/%s/hqdefault.jpg', $video_id ) ), - 'layout' => 'fill', - 'object-fit' => 'cover', - ]; - if ( ! empty( $props['title'] ) ) { - $img_attributes['alt'] = $props['title']; + $attributes = $this->prepare_attributes( $url, $video_id ); + + $props = $this->match_element_attributes( $html, Tag::IFRAME, self::IFRAME_ATTRIBUTES ); + foreach ( self::IFRAME_ATTRIBUTES as $iframe_prop ) { + if ( ! empty( $props[ $iframe_prop ] ) ) { + $attributes[ $iframe_prop ] = $props[ $iframe_prop ]; + } } - $img = AMP_HTML_Utils::build_tag( 'img', $img_attributes ); - $props['placeholder'] = AMP_HTML_Utils::build_tag( - 'a', - [ - 'placeholder' => '', - 'href' => esc_url_raw( $url ), - ], - $img + $placeholder = $this->get_placeholder_markup( $url, $video_id, $attributes ); + + return AMP_HTML_Utils::build_tag( Extension::YOUTUBE, $attributes, $placeholder ); + } + + /** + * Sanitize YouTube raw embeds. + * + * @param Document $dom Document. + * + * @return void + */ + public function sanitize_raw_embeds( Document $dom ) { + + $query_segments = array_map( + static function ( $domain ) { + return sprintf( + 'starts-with( @src, "https://www.%1$s/" ) or starts-with( @src, "https://%1$s/" ) or starts-with( @src, "http://www.%1$s/" ) or starts-with( @src, "http://%1$s/" )', + $domain + ); + }, + self::APPLICABLE_DOMAINS ); - return $props; + $query = implode( ' or ', $query_segments ); + + $nodes = $dom->xpath->query( sprintf( '//iframe[ %s ]', $query ) ); + + /** @var Element $node */ + foreach ( $nodes as $node ) { + + $amp_youtube_component = $this->get_amp_component( $dom, $node ); + + if ( ! empty( $amp_youtube_component ) ) { + $node->parentNode->replaceChild( $amp_youtube_component, $node ); + } + } } /** - * Render embed. + * Parse YouTube iframe element and return an AMP YouTube component. + * + * @param Document $dom Document DOM. + * @param Element $node YouTube iframe element. * - * @param array $args Args. - * @param string $url URL. - * @return string Rendered. + * @return Element|false AMP component, otherwise `false`. */ - public function render( $args, $url ) { - $args = wp_parse_args( - $args, - [ - 'video_id' => false, - 'layout' => 'responsive', - 'width' => $this->args['width'], - 'height' => $this->args['height'], - 'placeholder' => '', - ] + private function get_amp_component( Document $dom, Element $node ) { + + $url = $node->getAttribute( Attribute::SRC ); + $video_id = $this->get_video_id_from_url( $url ); + $attributes = $this->prepare_attributes( $url, $video_id ); + + foreach ( self::IFRAME_ATTRIBUTES as $iframe_prop ) { + if ( ! empty( $node->getAttribute( $iframe_prop ) ) ) { + $attributes[ $iframe_prop ] = $node->getAttribute( $iframe_prop ); + } + } + + if ( empty( $attributes[ Attribute::DATA_VIDEOID ] ) && empty( $attributes[ Attribute::DATA_LIVE_CHANNELID ] ) ) { + return false; + } + + $amp_node = AMP_DOM_Utils::create_node( + $dom, + Extension::YOUTUBE, + $attributes ); - if ( empty( $args['video_id'] ) ) { - return AMP_HTML_Utils::build_tag( - 'a', - [ - 'href' => esc_url_raw( $url ), - 'class' => 'amp-wp-embed-fallback', - ], - esc_html( $url ) + if ( $video_id && $amp_node instanceof Element ) { + $amp_node->appendChild( + $this->get_placeholder_element( $amp_node, $video_id, $attributes ) ); } - $this->did_convert_elements = true; + return $amp_node; + } - $attributes = array_merge( - [ 'data-videoid' => $args['video_id'] ], - wp_array_slice_assoc( $args, [ 'layout', 'width', 'height' ] ) + /** + * Prepare attributes for amp-youtube component. + * + * @param string $url YouTube video URL. + * @param string $video_id YouTube video ID. + * + * @return array prepared arguments for amp-youtube component. + */ + private function prepare_attributes( $url, $video_id = '' ) { + + $attributes = [ + Attribute::LAYOUT => Layout::RESPONSIVE, + Attribute::WIDTH => $this->args['width'], + Attribute::HEIGHT => $this->args['height'], + ]; + + if ( ! empty( $video_id ) ) { + $attributes[ Attribute::DATA_VIDEOID ] = $video_id; + } + + // Find start time of video. + $start_time = $this->get_start_time_from_url( $url ); + if ( ! empty( $start_time ) && 0 < (int) $start_time ) { + $attributes['data-param-start'] = (int) $start_time; + } + + $query_vars = []; + $query_param = wp_parse_url( $url, PHP_URL_QUERY ); + wp_parse_str( $query_param, $query_vars ); + $query_vars = ( is_array( $query_vars ) ) ? $query_vars : []; + + $excluded_param = [ 'start', 'v', 'vi', 'w', 'h' ]; + + foreach ( $query_vars as $key => $value ) { + + if ( in_array( $key, $excluded_param, true ) ) { + continue; + } + + if ( in_array( $key, [ Attribute::AUTOPLAY, Attribute::LOOP ], true ) ) { + $attributes[ $key ] = $value; + continue; + } + + if ( 'channel' === $key ) { + $attributes[ Attribute::DATA_LIVE_CHANNELID ] = $value; + continue; + } + + $attributes[ sanitize_key( "data-param-$key" ) ] = $value; + } + + return $attributes; + } + + /** + * Placeholder element for AMP YouTube component in the DOM. + * + * @param Element $amp_component AMP component element. + * @param string $video_id Video ID. + * @param array $attributes YouTube attributes. + * + * @return Element Placeholder. + */ + private function get_placeholder_element( Element $amp_component, $video_id, $attributes ) { + $dom = Document::fromNode( $amp_component ); + + $img_attributes = [ + Attribute::SRC => esc_url_raw( sprintf( 'https://i.ytimg.com/vi/%s/hqdefault.jpg', $video_id ) ), + Attribute::LAYOUT => Layout::FILL, + Attribute::OBJECT_FIT => 'cover', + ]; + + if ( $attributes[ Attribute::TITLE ] ) { + $img_attributes[ Attribute::ALT ] = $attributes[ Attribute::TITLE ]; + } + + $img_node = AMP_DOM_Utils::create_node( + $dom, + Tag::IMG, + $img_attributes ); - if ( ! empty( $args['title'] ) ) { - $attributes['title'] = $args['title']; + + $video_url = esc_url_raw( sprintf( 'https://www.youtube.com/watch?v=%s', $video_id ) ); + if ( array_key_exists( 'data-param-start', $attributes ) ) { + $video_url .= '#t=' . $attributes['data-param-start']; + } + + $placeholder = AMP_DOM_Utils::create_node( + $dom, + Tag::A, + [ + Attribute::PLACEHOLDER => '', + Attribute::HREF => $video_url, + ] + ); + + $placeholder->appendChild( $img_node ); + + return $placeholder; + } + + /** + * To get placeholder for AMP component as constructed HTML string. + * + * @param string $url YouTube URL. + * @param string $video_id Video ID. + * @param array $attributes YouTube attributes. + * + * @return string HTML string. + */ + private function get_placeholder_markup( $url, $video_id, $attributes ) { + + $img_attributes = [ + Attribute::SRC => esc_url_raw( sprintf( 'https://i.ytimg.com/vi/%s/hqdefault.jpg', $video_id ) ), + Attribute::LAYOUT => Layout::FILL, + Attribute::OBJECT_FIT => 'cover', + ]; + + if ( ! empty( $attributes[ Attribute::TITLE ] ) ) { + $img_attributes[ Attribute::ALT ] = $attributes[ Attribute::TITLE ]; } - return AMP_HTML_Utils::build_tag( 'amp-youtube', $attributes, $args['placeholder'] ); + $img = ''; + + return AMP_HTML_Utils::build_tag( + Tag::A, + [ + Attribute::PLACEHOLDER => '', + Attribute::HREF => esc_url_raw( $url ), + ], + $img + ); } /** * Determine the video ID from the URL. * * @param string $url URL. + * * @return string|false Video ID, or false if none could be retrieved. */ private function get_video_id_from_url( $url ) { + $parsed_url = wp_parse_url( $url ); if ( ! isset( $parsed_url['host'] ) ) { return false; } - $domain = implode( '.', array_slice( explode( '.', $parsed_url['host'] ), -2 ) ); - if ( ! in_array( $domain, [ 'youtu.be', 'youtube.com', 'youtube-nocookie.com' ], true ) ) { + $domain = implode( '.', array_slice( explode( '.', $parsed_url['host'] ), - 2 ) ); + if ( ! in_array( $domain, self::APPLICABLE_DOMAINS, true ) ) { return false; } @@ -230,12 +413,62 @@ private function get_video_id_from_url( $url ) { // Other top-level segments indicate non-video URLs. There are examples of URLs having segments including // 'v', 'vi', and 'e' but these do not work anymore. In any case, they are added here for completeness. if ( ! empty( $segments[1] ) && in_array( $segments[0], [ 'embed', 'watch', 'v', 'vi', 'e' ], true ) ) { + + /** + * Ignore live streaming channel URLs. For example: + * * https://www.youtube.com/embed/live_stream?channel=UCkaNo2FUEWips2z4BkOHl6Q + */ + if ( 'embed' === $segments[0] && 'live_stream' === $segments[1] && isset( $query_vars['channel'] ) ) { + return false; + } + return $segments[1]; } return false; } + /** + * Get the start time of the YouTube video in seconds. + * + * @param string $url YouTube URL. + * + * @return int Start time in seconds. + */ + private function get_start_time_from_url( $url ) { + + $start_time = 0; + $parsed_url = wp_parse_url( $url ); + + if ( ! empty( $parsed_url['query'] ) ) { + $query_vars = []; + wp_parse_str( $parsed_url['query'], $query_vars ); + + if ( ! empty( $query_vars['start'] ) && 0 < (int) $query_vars['start'] ) { + return (int) $query_vars['start']; + } + } + + if ( ! empty( $parsed_url['fragment'] ) ) { + $regex = '/^t=(?:(?\d+)m)?(?:(?\d+)s?)?$/'; + + preg_match( $regex, $parsed_url['fragment'], $matches ); + + if ( is_array( $matches ) ) { + $matches = wp_parse_args( + $matches, + [ + 'minutes' => 0, + 'seconds' => 0, + ] + ); + $start_time = ( (int) $matches['seconds'] + ( (int) $matches['minutes'] * 60 ) ); + } + } + + return $start_time; + } + /** * Override the output of YouTube videos. * @@ -244,17 +477,22 @@ private function get_video_id_from_url( $url ) { * * @param string $html Empty variable to be replaced with shortcode markup. * @param array $attr The shortcode attributes. + * * @return string|null $markup The markup to output. */ public function video_override( $html, $attr ) { - if ( ! isset( $attr['src'] ) ) { + + if ( ! isset( $attr[ Attribute::SRC ] ) ) { return $html; } - $video_id = $this->get_video_id_from_url( $attr['src'] ); + + $src = $attr[ Attribute::SRC ]; + $video_id = $this->get_video_id_from_url( $src ); + if ( ! $video_id ) { return $html; } - return $this->render( compact( 'video_id' ), $attr['src'] ); + return $this->render( $html, $src, $video_id ); } } diff --git a/includes/utils/class-amp-dom-utils.php b/includes/utils/class-amp-dom-utils.php index 164abfc2c3f..a3dca61073f 100644 --- a/includes/utils/class-amp-dom-utils.php +++ b/includes/utils/class-amp-dom-utils.php @@ -158,7 +158,7 @@ public static function get_content_from_dom_node( Document $dom, $node ) { * @param string $tag A valid HTML element tag for the element to be added. * @param string[] $attributes One of more valid attributes for the new node. * - * @return DOMElement|false The DOMElement for the given $tag, or false on failure + * @return Element|false The element for the given $tag, or false on failure */ public static function create_node( Document $dom, $tag, $attributes ) { $node = $dom->createElement( $tag ); diff --git a/includes/validation/class-amp-validation-error-taxonomy.php b/includes/validation/class-amp-validation-error-taxonomy.php index d8c5e96a69a..4eac9a96dc2 100644 --- a/includes/validation/class-amp-validation-error-taxonomy.php +++ b/includes/validation/class-amp-validation-error-taxonomy.php @@ -128,7 +128,7 @@ class AMP_Validation_Error_Taxonomy { * This is also used in WP_List_Table, like for the 'Bulk Actions' option. * When this is present, this ensures that this isn't filtered. * - * @var int + * @var string */ const NO_FILTER_VALUE = ''; @@ -1095,7 +1095,7 @@ public static function add_term_filter_query_var( $url, $tax ) { && in_array( $_POST[ self::VALIDATION_ERROR_TYPE_QUERY_VAR ], // phpcs:ignore WordPress.Security.NonceVerification.Missing - array_merge( self::get_error_types(), [ (string) self::NO_FILTER_VALUE ] ), + array_merge( self::get_error_types(), [ self::NO_FILTER_VALUE ] ), true ) ) { @@ -1457,7 +1457,7 @@ private static function output_error_status_filter_option_markup( $option_text, /** * Gets all of the possible error types. * - * @return array Error types. + * @return string[] Error types. */ public static function get_error_types() { return [ self::HTML_ELEMENT_ERROR_TYPE, self::HTML_ATTRIBUTE_ERROR_TYPE, self::JS_ERROR_TYPE, self::CSS_ERROR_TYPE ]; diff --git a/src/BackgroundTask/MonitorCssTransientCaching.php b/src/BackgroundTask/MonitorCssTransientCaching.php index 309b4f76311..a2965aefe84 100644 --- a/src/BackgroundTask/MonitorCssTransientCaching.php +++ b/src/BackgroundTask/MonitorCssTransientCaching.php @@ -108,7 +108,6 @@ public function process( ...$args ) { $date = isset( $args[0] ) && $args[0] instanceof DateTimeInterface ? $args[0] : new DateTimeImmutable(); - /** @phpstan-ignore-next-line */ $transient_count = isset( $args[1] ) ? (int) $args[1] : $this->query_css_transient_count(); $date_string = $date->format( 'Ymd' ); diff --git a/tests/php/test-class-amp-youtube-embed-handler.php b/tests/php/test-class-amp-youtube-embed-handler.php index 189b2d4b86d..ff0348864cc 100644 --- a/tests/php/test-class-amp-youtube-embed-handler.php +++ b/tests/php/test-class-amp-youtube-embed-handler.php @@ -13,7 +13,7 @@ /** * Tests for AMP_YouTube_Embed_Handler. * - * @covers AMP_YouTube_Embed_Handler + * @coversDefaultClass \AMP_YouTube_Embed_Handler */ class Test_AMP_YouTube_Embed_Handler extends TestCase { @@ -35,7 +35,7 @@ class Test_AMP_YouTube_Embed_Handler extends TestCase { /** * An instance of this embed handler. * - * @var AMP_YouTube_Embed_Handler. + * @var AMP_YouTube_Embed_Handler */ public $handler; @@ -100,10 +100,90 @@ public function mock_http_request( $preempt, $r, $url ) { ]; } + /** + * @covers ::register_embed() + * @covers ::unregister_embed() + */ + public function test_register_and_unregister_embed() { + $embed = new AMP_YouTube_Embed_Handler(); + $embed->register_embed(); + $this->assertEquals( 10, has_filter( 'embed_oembed_html', [ $embed, 'filter_embed_oembed_html' ] ) ); + $this->assertEquals( 10, has_filter( 'wp_video_shortcode_override', [ $embed, 'video_override' ] ) ); + $embed->unregister_embed(); + $this->assertFalse( has_filter( 'embed_oembed_html', [ $embed, 'filter_embed_oembed_html' ] ) ); + $this->assertFalse( has_filter( 'wp_video_shortcode_override', [ $embed, 'video_override' ] ) ); + } + + /** + * Data provider for $this->test_sanitize_raw_embeds() + * + * @return string[][] + */ + public function sanitize_raw_embeds_data_provider() { + + return [ + 'youtube-embed' => [ + 'source' => '', + 'expected' => 'YouTube video player', + ], + 'youtube-start' => [ + 'source' => '', + 'expected' => 'YouTube video player', + ], + 'with_http' => [ + 'source' => '', + 'expected' => 'YouTube video player', + ], + 'short-url' => [ + 'source' => '', + 'expected' => 'YouTube video player', + ], + 'live_streaming_channel' => [ + 'source' => '', + 'expected' => '', + ], + 'none-youtube' => [ + 'source' => '', + 'expected' => '', + ], + 'youtube-unknown' => [ + 'source' => '', + 'expected' => '', + ], + ]; + } + + /** + * @dataProvider sanitize_raw_embeds_data_provider + * + * @covers ::sanitize_raw_embeds() + * @covers ::get_amp_component() + * @covers ::get_placeholder_element() + * @covers ::prepare_attributes() + */ + public function test_sanitize_raw_embeds( $source, $expected ) { + + $embed = new AMP_YouTube_Embed_Handler(); + $embed->register_embed(); + + $dom = AMP_DOM_Utils::get_dom_from_content( $source ); + $embed->sanitize_raw_embeds( $dom ); + + $layout_sanitizer = new AMP_Layout_Sanitizer( $dom ); + $layout_sanitizer->sanitize(); + + $content = AMP_DOM_Utils::get_content_from_dom( $dom ); + + $this->assertEquals( $expected, trim( $content ) ); + } + /** * Test video_override(). * - * @covers AMP_YouTube_Embed_Handler::video_override() + * @covers ::register_embed() + * @covers ::video_override() + * @covers ::render() + * @covers ::get_placeholder_markup() */ public function test_video_override() { remove_all_filters( 'wp_video_shortcode_override' ); @@ -138,6 +218,7 @@ public function test_video_override() { remove_all_filters( 'wp_video_shortcode_override' ); } + /** @return array */ public function get_conversion_data() { return [ 'no_embed' => [ @@ -147,20 +228,20 @@ public function get_conversion_data() { 'url_simple' => [ 'https://www.youtube.com/watch?v=kfVsfOSbJY0' . PHP_EOL, - '

Rebecca Black - Friday

' . PHP_EOL, - '

' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, ], 'url_short' => [ 'https://youtu.be/kfVsfOSbJY0' . PHP_EOL, - '

Rebecca Black - Friday

' . PHP_EOL, - '

' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, ], 'url_with_querystring' => [ 'http://www.youtube.com/watch?v=kfVsfOSbJY0&hl=en&fs=1&w=425&h=349' . PHP_EOL, - '

Rebecca Black - Friday

' . PHP_EOL, - '

' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, ], // Several reports of invalid URLs that have multiple `?` in the URL. @@ -171,14 +252,48 @@ public function get_conversion_data() { 'embed_url' => [ 'https://www.youtube.com/embed/kfVsfOSbJY0' . PHP_EOL, - '

Rebecca Black - Friday

' . PHP_EOL, - '

' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, + ], + + 'with_start_time' => [ + 'http://www.youtube.com/watch?v=kfVsfOSbJY0#t=1m10s' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, + ], + + 'with_start_time_2' => [ + 'http://www.youtube.com/watch?v=kfVsfOSbJY0#t=62' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, + ], + + 'with_start_time_3' => [ + 'http://www.youtube.com/watch?v=kfVsfOSbJY0#t=62s' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, + ], + + 'with_start_time_4' => [ + 'http://www.youtube.com/watch?v=kfVsfOSbJY0#t=1m' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, + ], + + 'with_start_time_5' => [ + 'http://www.youtube.com/watch?v=kfVsfOSbJY0#t=1m2' . PHP_EOL, + '

Rebecca Black - Friday

' . PHP_EOL, + '

' . PHP_EOL, ], ]; } /** * @dataProvider get_conversion_data + * @covers ::register_embed() + * @covers ::filter_embed_oembed_html() + * @covers ::render() + * @covers ::get_placeholder_markup() */ public function test__conversion( $source, $expected, $fallback_for_expected = null ) { $this->handler->register_embed(); @@ -281,6 +396,10 @@ public function get_video_id_data() { 'http://youtube.com/?wrong=XOY3ZUO6P0k', false, ], + 'live_streaming_channel' => [ + 'https://www.youtube.com/embed/live_stream?channel=UCkaNo2FUEWips2z4BkOHl6Q', + false, + ], ]; } @@ -288,7 +407,7 @@ public function get_video_id_data() { * Tests get_video_id_from_url. * * @dataProvider get_video_id_data - * @covers AMP_YouTube_Embed_Handler::get_video_id_from_url() + * @covers ::get_video_id_from_url() * * @param string $url The URL to test. * @param string|false $expected The expected result. @@ -301,6 +420,53 @@ public function test_get_video_id_from_url( $url, $expected ) { ); } + /** + * Gets the test data for test_get_start_time_from_url(). + * + * @return array The test data. + */ + public function get_start_time_data() { + + return [ + 'empty_because_no_start_data' => [ + 'http://youtube.com/?wrong=XOY3ZUO6P0k', + 0, + ], + 'data_in_query_string' => [ + 'http://www.youtube.com/watch?v=0zM3nApSvMg&start=90', + 90, + ], + 'data_in_fragment' => [ + 'http://www.youtube.com/watch?v=0zM3nApSvMg#t=0m10s', + 10, + ], + 'data_in_fragment_with_minutes' => [ + 'http://www.youtube.com/watch?v=0zM3nApSvMg#t=1m10s', + 70, + ], + ]; + } + + /** + * Tests get_start_time_from_url. + * + * @dataProvider get_start_time_data + * @covers ::get_start_time_from_url() + * + * @param string $url The URL to test. + * @param string|false $expected The expected result. + * + * @throws ReflectionException If a reflection of the object is not possible. + */ + public function test_get_start_time_from_url( $url, $expected ) { + + $this->assertEquals( + $expected, + $this->call_private_method( $this->handler, 'get_start_time_from_url', [ $url ] ) + ); + } + + /** @return array */ public function get_scripts_data() { return [ 'not_converted' => [ @@ -316,6 +482,8 @@ public function get_scripts_data() { /** * @dataProvider get_scripts_data + * @covers ::register_embed() + * @covers ::get_scripts() */ public function test__get_scripts( $source, $expected ) { $this->handler->register_embed();