diff --git a/Makefile b/Makefile index 124b479..24784fb 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: % dist-clean dist make-zip svn +.PHONY: % dist-clean dist make-zip svn test check fix FILE := image-cdn-0.0.0.zip @@ -14,6 +14,15 @@ make-zip: cd dist && zip -9 -r ${FILE} image_cdn rm -rf dist/image_cdn -svn: - cp -v -r plugin-assets/* svn/assets/ - cp -v -r *.php *.txt imageengine assets templates svn/trunk +test: + vendor/bin/phpunit -vvv -c phpunit-standalone.xml.dist + +fix: + vendor/bin/phpcbf -v + +check: + vendor/bin/phpcs + +# svn: +# cp -v -r plugin-assets/* svn/assets/ +# cp -v -r *.php *.txt imageengine assets templates svn/trunk diff --git a/image-cdn.php b/image-cdn.php index 2eff73d..6b5c8e1 100644 --- a/image-cdn.php +++ b/image-cdn.php @@ -16,9 +16,15 @@ * Requires PHP: 5.6 * Text Domain: image-cdn * License: GPLv2 or later - * Version: 1.1.0 + * Version: 1.1.1 */ +// Update this then you update "Requires at least" above! +define( 'IMAGE_CDN_MIN_WP', '4.6' ); + +// Update this when you update the "Version" above! +define( 'IMAGE_CDN_VERSION', '1.1.1' ); + // Load plugin files. require_once __DIR__ . '/imageengine/class-settings.php'; require_once __DIR__ . '/imageengine/class-rewriter.php'; @@ -29,7 +35,6 @@ define( 'IMAGE_CDN_FILE', __FILE__ ); define( 'IMAGE_CDN_DIR', __DIR__ ); define( 'IMAGE_CDN_BASE', plugin_basename( __FILE__ ) ); -define( 'IMAGE_CDN_MIN_WP', '3.8' ); add_action( 'plugins_loaded', array( ImageEngine\ImageCDN::class, 'instance' ) ); register_uninstall_hook( __FILE__, array( ImageEngine\ImageCDN::class, 'handle_uninstall_hook' ) ); diff --git a/imageengine/class-imagecdn.php b/imageengine/class-imagecdn.php index 11ade71..72db95b 100644 --- a/imageengine/class-imagecdn.php +++ b/imageengine/class-imagecdn.php @@ -1,6 +1,6 @@ ; rel=preconnect' ); - header( 'Feature-Policy: ch-viewport-width ' . $protocol . $host . '; ch-width ' . $protocol . $host . '; ch device-memory ' . $protocol . $host . '; ch-dpr ' . $protocol . $host . '; ch-downlink ' . $protocol . $host . '; ch-ect ' . $protocol . $host . ';' ); + if ( empty( $host ) ) { + return; + } + + $protocol = is_ssl() ? 'https' : 'http'; + + // Add Preconnect header. + self::header( 'Link', "<{$protocol}://{$host}>; rel=preconnect" ); + + // Add Feature-Policy header. + // @deprecated in favor of Permissions-Policy and will be removed once adaquate market + // adoption has been reached (90-95%). + $features = array(); + foreach ( self::$client_hints as $hint ) { + $features[] = strtolower( "ch-{$hint} {$protocol}://{$host}" ); } + self::header( 'Feature-Policy', strtolower( implode( '; ', $features ) ) ); + + $permissions = array(); + foreach ( self::$client_hints as $hint ) { + $permissions[] = strtolower( "ch-{$hint}=(\"{$protocol}://{$host}\")" ); + } + // Add Permissions-Policy header. + // This header replaced Feature-Policy in Chrome 88, released in January 2021. + // @see https://github.com/w3c/webappsec-permissions-policy/blob/main/permissions-policy-explainer.md#appendix-big-changes-since-this-was-called-feature-policy . + self::header( 'Permissions-Policy', strtolower( implode( ', ', $permissions ) ) ); } @@ -84,7 +175,7 @@ public static function add_headers() { */ public static function add_head_tags() { // Add client hints. - echo ' ' . "\n"; + echo ' ' . "\n"; // Add resource hints. $options = self::get_options(); @@ -118,6 +209,34 @@ public static function add_action_link( $data ) { ); } + /** + * Gets the list of active client hints. + * + * @return array client hints. + */ + public static function get_client_hints() { + return self::$client_hints; + } + + /** + * Gets the list of safe client hints. + * + * @return array client hints. + */ + public static function get_safe_client_hints() { + return self::$safe_client_hints; + } + + /** + * Gets the list of active unsafe client hints. + * + * @return array client hints. + */ + public static function get_unsafe_client_hints() { + return array_diff( self::$client_hints, self::$safe_client_hints ); + } + + /** * Rewrite image URLs in REST API responses * @@ -268,6 +387,9 @@ public static function register_textdomain() { * @return array $diff data pairs. */ public static function get_options() { + if ( self::$tests_running ) { + return self::$test_options; + } return wp_parse_args( get_option( 'image_cdn' ), self::default_options() ); } diff --git a/imageengine/class-rewriter.php b/imageengine/class-rewriter.php index b63a511..ed6a9b5 100644 --- a/imageengine/class-rewriter.php +++ b/imageengine/class-rewriter.php @@ -115,12 +115,19 @@ public function __construct( * @return boolean true if need to be excluded. */ public function exclude_asset( $asset ) { + $path = strtolower( wp_parse_url( $asset, PHP_URL_PATH ) ); + // Excludes. foreach ( $this->excludes as $exclude ) { - if ( $exclude && stripos( $asset, $exclude ) !== false ) { + if ( '' === $exclude ) { + continue; + } + + if ( false !== strpos( $path, strtolower( $exclude ) ) ) { return true; } } + return false; } diff --git a/imageengine/class-settings.php b/imageengine/class-settings.php index bfd7b3b..d2ef9b8 100644 --- a/imageengine/class-settings.php +++ b/imageengine/class-settings.php @@ -254,17 +254,39 @@ public static function test_config() { if ( ! isset( $cdn_res['headers']['content-type'] ) ) { $out['type'] = 'warning'; - $out['message'] = 'Unable to confirm that the CDN is working properly because it didn\'t send a content type'; + $out['message'] = 'Unable to confirm that the CDN is working properly because it didn\'t send Content-Type'; wp_send_json_error( $out ); } $cdn_type = $cdn_res['headers']['content-type']; if ( strpos( $cdn_type, 'image/png' ) === false ) { $out['type'] = 'error'; - $out['message'] = "CDN returned the wrong content type (expected 'image/png', got '$cdn_type'"; + $out['message'] = "CDN returned the wrong content type (expected 'image/png', got '$cdn_type')"; wp_send_json_error( $out ); } + /** + * This check it commented out until we can confirm that it properly tests CORS functionality. + * + * $unsafe_hints = ImageCDN::get_unsafe_client_hints(); + * if ( 0 < count( $unsafe_hints ) ) { + * $cors_error = true; + * if ( ! array_key_exists( 'access-control-allowed-headers', $cdn_res['headers'] ) ) { + * $out['type'] = 'warning'; + * $out['message'] = 'Unable to confirm that the CDN supports client-hints because it didn\'t send Access-Control-Allow-Headers. Fonts may not work if served from the CDN.'; + * wp_send_json_error( $out ); + * } + * + * $allowed = preg_split( '/ +/', trim( $cdn_res['headers']['access-control-allowed-headers'] ) ); + * $missing_hints = array_diff( $unsafe_hints, $allowed ); + * if ( 0 < count( $missing_hints ) ) { + * $out['type'] = 'warning'; + * $out['message'] = 'Unable to confirm that the CDN supports advanced client-hints because it is missing some active client-hints in Access-Control-Allow-Headers (' . implode( ',', $missing_hints ) . '). Fonts may not work if served from the CDN.'; + * wp_send_json_error( $out ); + * } + * } + */ + $out['type'] = 'success'; $out['message'] = 'Test successful'; wp_send_json_success( $out ); diff --git a/readme.txt b/readme.txt index f980703..0a00e6d 100644 --- a/readme.txt +++ b/readme.txt @@ -120,6 +120,11 @@ Upgrades can be performed in the normal WordPress way, nothing else will need to == Changelog == += 1.1.1 = +* Improved CORS compatibility +* Removed downlink and ect hint +* Added Permissions-Policy header + = 1.1.0 = * Fixed compatibility issue with Divi * Increased performance diff --git a/templates/settings.php b/templates/settings.php index 5df8e29..03740ef 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -18,11 +18,11 @@ ); ?>

-

:

+

  1. Sign up for an ImageEngine account
  2. -

    See full documentation.

    -

    +

    + +

    @@ -119,11 +127,11 @@

    - , + ,

    diff --git a/tests/standalone/bootstrap.php b/tests/standalone/bootstrap.php index 41f861c..3beb92e 100644 --- a/tests/standalone/bootstrap.php +++ b/tests/standalone/bootstrap.php @@ -17,3 +17,11 @@ function is_admin_bar_showing() { return false; } + +function wp_parse_url($url, $flags) { + return parse_url($url, $flags); +} + +function is_ssl() { + return true; +} \ No newline at end of file diff --git a/tests/standalone/test-imagecdn.php b/tests/standalone/test-imagecdn.php new file mode 100644 index 0000000..472307e --- /dev/null +++ b/tests/standalone/test-imagecdn.php @@ -0,0 +1,32 @@ + 'https://foo.com', + ]; + + ImageCDN::add_headers(); + + $expected = [ + 'Accept-CH: viewport-width, width, dpr', + 'Link: ; rel=preconnect', + 'Feature-Policy: ch-viewport-width https://foo.com; ch-width https://foo.com; ch-dpr https://foo.com', + 'Permissions-Policy: ch-viewport-width=("https://foo.com"), ch-width=("https://foo.com"), ch-dpr=("https://foo.com")', + ]; + $this->assertSame($expected, ImageCDN::$test_headers_written); + } +} \ No newline at end of file diff --git a/tests/standalone/test-rewriter.php b/tests/standalone/test-rewriter.php index 0b1526c..7629c05 100644 --- a/tests/standalone/test-rewriter.php +++ b/tests/standalone/test-rewriter.php @@ -20,15 +20,17 @@ public function testExcludeAsset() { $cdn_url = 'http://my.cdn'; $path = ''; $dirs = 'wp-includes'; - $excludes = array( '.php' ); + $excludes = array( '.php', '.woff2' ); $relative = true; $https = true; $directives = '/cmpr_20'; $rewrite = new Rewriter( $blog_url, $cdn_url, $path, $dirs, $excludes, $relative, $https, $directives ); - $this->assertEquals( true, $rewrite->exclude_asset( '/wp-includes/bar.php' ) ); - $this->assertEquals( true, $rewrite->exclude_asset( '/wp-includes/bar.php/foo.jpg' ) ); + $this->assertEquals( true, $rewrite->exclude_asset( '/wp-includes/bar.php' ) ); + $this->assertEquals( true, $rewrite->exclude_asset( '/wp-includes/bar.woff2' ) ); + $this->assertEquals( false, $rewrite->exclude_asset( '/wp-includes/bar.woff' ) ); + $this->assertEquals( true, $rewrite->exclude_asset( '/wp-includes/bar.php/foo.jpg' ) ); $this->assertEquals( false, $rewrite->exclude_asset( '/wp-includes/bar.jpg' ) ); }