From 46bb9d57358892ca7e3f3879e6a1d82e01c66c69 Mon Sep 17 00:00:00 2001 From: Caspar Green Date: Fri, 26 Feb 2021 13:42:30 -0500 Subject: [PATCH] Closes #3423 Add preconnect tag for CDN URLs (#3463) * Add add_preconnect_cdn() method * Add integration test draft and notes. * Implement preconnect for non-crossorigin cdn-urls * Add fixture, adjust integration test * Revise forcorrect cdn urls, use crossorigin second * Run phpcbf * Update CDN preconnect testcases * Use agnostic scheme when not provided * Rerun phpcs * Clean dns-prefetch out of testcases :) * Add testcase/solution for `test/tests` --- inc/Engine/CDN/Subscriber.php | 76 ++++++++++++++++++- .../CDN/Subscriber/add-preconnect-cdn.php | 23 ++++++ .../inc/Engine/CDN/Subscriber/TestCase.php | 2 +- .../CDN/Subscriber/addPreconnectCdn.php | 45 +++++++++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/inc/Engine/CDN/Subscriber/add-preconnect-cdn.php create mode 100644 tests/Integration/inc/Engine/CDN/Subscriber/addPreconnectCdn.php diff --git a/inc/Engine/CDN/Subscriber.php b/inc/Engine/CDN/Subscriber.php index ad96701c50..5d6c0ee175 100644 --- a/inc/Engine/CDN/Subscriber.php +++ b/inc/Engine/CDN/Subscriber.php @@ -28,7 +28,7 @@ class Subscriber implements Subscriber_Interface { * Constructor * * @param Options_Data $options WP Rocket Options instance. - * @param CDN $cdn CDN instance. + * @param CDN $cdn CDN instance. */ public function __construct( Options_Data $options, CDN $cdn ) { $this->options = $options; @@ -55,6 +55,7 @@ public static function get_subscribed_events() { 'rocket_css_url' => [ 'add_cdn_url', 10, 2 ], 'rocket_js_url' => [ 'add_cdn_url', 10, 2 ], 'rocket_asset_url' => [ 'maybe_replace_url', 10, 2 ], + 'wp_resource_hints' => [ 'add_preconnect_cdn', 10, 2 ], ]; } @@ -64,6 +65,7 @@ public static function get_subscribed_events() { * @since 3.4 * * @param string $html HTML content. + * * @return string */ public function rewrite( $html ) { @@ -80,6 +82,7 @@ public function rewrite( $html ) { * @since 3.4.0.4 * * @param string $html HTML content. + * * @return string */ public function rewrite_srcset( $html ) { @@ -96,6 +99,7 @@ public function rewrite_srcset( $html ) { * @since 3.4 * * @param string $content CSS content. + * * @return string */ public function rewrite_css_properties( $content ) { @@ -155,6 +159,7 @@ public function get_cdn_hosts( array $hosts = [], array $zones = [ 'all' ] ) { * @since 3.4 * * @param array $domains Domain names to DNS prefetch. + * * @return array */ public function add_dns_prefetch_cdn( $domains ) { @@ -176,8 +181,9 @@ public function add_dns_prefetch_cdn( $domains ) { * * @since 3.4 * - * @param string $url URL to rewrite. + * @param string $url URL to rewrite. * @param string $original_url Original URL for this URL. Optional. + * * @return string */ public function add_cdn_url( $url, $original_url = '' ) { @@ -195,8 +201,9 @@ public function add_cdn_url( $url, $original_url = '' ) { * * @since 3.5.3 * - * @param string $url URL of the asset. + * @param string $url URL of the asset. * @param array $zones Array of corresponding zones for the asset. + * * @return string */ public function maybe_replace_url( $url, array $zones = [ 'all' ] ) { @@ -241,6 +248,69 @@ public function maybe_replace_url( $url, array $zones = [ 'all' ] ) { return $url; } + /** + * Add a preconnect tag for the CDN. + * + * @since 3.8.3 + * + * @param array $urls The initial array of wp_resource_hint urls. + * @param string $relation_type The relation type for the hint: eg., 'preconnect', 'prerender', etc. + * + * @return array The filtered urls. + */ + public function add_preconnect_cdn( array $urls, string $relation_type ): array { + if ( + 'preconnect' !== $relation_type + || + rocket_bypass() + || + ! $this->is_allowed() + || + ! $this->is_cdn_enabled() + ) { + return $urls; + } + + $cdn_urls = $this->cdn->get_cdn_urls( [ 'all', 'images', 'css_and_js', 'css', 'js' ] ); + + if ( empty( $cdn_urls ) ) { + return $urls; + } + + foreach ( $cdn_urls as $url ) { + $url_parts = get_rocket_parse_url( $url ); + + if ( empty( $url_parts['scheme'] ) ) { + if ( preg_match( '/^(?![\/])(?=[^\.]+\/).+/i', $url ) ) { + continue; + } + + $url = '//' . $url; + $url_parts = get_rocket_parse_url( $url ); + } + + $domain = empty( $url_parts['scheme'] ) + ? '//' . $url_parts['host'] + : $url_parts['scheme'] . '://' . $url_parts['host']; + + // Note: As of 22 Feb, 2021 we cannot add more than one instance of a domain url + // on the wp_resource_hint() hook -- wp_resource_hint() will + // only actually print the first one. + // Ideally, we want both because CSS resources will use the crossorigin version, + // But JS resources will not. + // Jonathan has submitted a ticket to change this behavior: + // @see https://core.trac.wordpress.org/ticket/52465 + // Until then, we order these to prefer/print the non-crossorigin version. + $urls[] = [ 'href' => $domain ]; + $urls[] = [ + 'href' => $domain, + 'crossorigin' => 'anonymous', + ]; + } + + return $urls; + } + /** * Checks if CDN can be applied * diff --git a/tests/Fixtures/inc/Engine/CDN/Subscriber/add-preconnect-cdn.php b/tests/Fixtures/inc/Engine/CDN/Subscriber/add-preconnect-cdn.php new file mode 100644 index 0000000000..127196972b --- /dev/null +++ b/tests/Fixtures/inc/Engine/CDN/Subscriber/add-preconnect-cdn.php @@ -0,0 +1,23 @@ + [ + 'cdn-cnames' => [ + '123456.rocketcdn.me', + 'https://my-cdn.cdnservice.com/', + 'http://cdn.example.com', + '/some/path/with/no/domain', + 'test/tests', + '8901.wicked-fast-cdn.com/path/to/my/files', + 'https://another.cdn.com/with/a/path', + ], + 'expected-html' => << + + + + + +HTML + ], +]; diff --git a/tests/Integration/inc/Engine/CDN/Subscriber/TestCase.php b/tests/Integration/inc/Engine/CDN/Subscriber/TestCase.php index 6c77f16329..f65f0d2013 100644 --- a/tests/Integration/inc/Engine/CDN/Subscriber/TestCase.php +++ b/tests/Integration/inc/Engine/CDN/Subscriber/TestCase.php @@ -2,7 +2,7 @@ namespace WP_Rocket\Tests\Integration\inc\Engine\CDN\Subscriber; -use WPMedia\PHPUnit\Integration\TestCase as BaseTestCase; +use WP_Rocket\Tests\Integration\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { protected $cnames; diff --git a/tests/Integration/inc/Engine/CDN/Subscriber/addPreconnectCdn.php b/tests/Integration/inc/Engine/CDN/Subscriber/addPreconnectCdn.php new file mode 100644 index 0000000000..6934d381fd --- /dev/null +++ b/tests/Integration/inc/Engine/CDN/Subscriber/addPreconnectCdn.php @@ -0,0 +1,45 @@ +unregisterAllCallbacksExcept( 'wp_resource_hints', 'add_preconnect_cdn', 10 ); + + parent::setUp(); + } + + public function tearDown() { + $this->restoreWpFilter( 'wp_resource_hints' ); + + parent::tearDown(); + } + + /** + * @dataProvider providerTestData + */ + public function testShouldAddPreconnectCdn($cnames, $expected) { + $this->cnames = $cnames; + + add_filter( 'pre_get_rocket_option_cdn', [ $this, 'return_true'] ); + add_filter( 'rocket_cdn_cnames', [ $this, 'setCnames' ] ); + + ob_start(); + wp_resource_hints(); + + $this->assertSame( + $this->format_the_html($expected), + $this->format_the_html(ob_get_clean()) + ); + } + + public function providerTestData() { + return $this->getTestData( __DIR__, 'add-preconnect-cdn' ); + } +}