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' );
+ }
+}