Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor client-hints, add Permissions-Policy #12

Merged
merged 9 commits into from Feb 24, 2021
17 changes: 13 additions & 4 deletions 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

Expand All @@ -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
9 changes: 7 additions & 2 deletions image-cdn.php
Expand Up @@ -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';
Expand All @@ -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' ) );
Expand Down
144 changes: 133 additions & 11 deletions imageengine/class-imagecdn.php
@@ -1,6 +1,6 @@
<?php
/**
* This file contains the ImageCDN class
* This file contains the ImageCDN class.
*
* @package ImageCDN
*/
Expand All @@ -21,12 +21,65 @@ public static function instance() {
}

/**
* Singleton Rewriter instance
* Client hints.
*
* @var []string
*/
private static $client_hints = array(
'Viewport-Width',
'Width',
'DPR',
/**
* Disabled for CORS compatibility:
* 'ECT',
* 'Device-Memory',
* 'RTT',
* 'Downlink',
*/
);

/**
* Client hints that do not require CORS preflight confirmation.
*
* @var []string
*/
private static $safe_client_hints = array(
'Viewport-Width',
'Width',
'DPR',
);

/**
* Singleton Rewriter instance.
*
* @var Rewriter
*/
private static $rewriter;

/**
* If true, some functionality will be augmented to facilitate testing.
*
* @internal
* @var bool
*/
public static $tests_running = false;

/**
* Captures headers written during unit testing.
*
* @internal
* @var []string
*/
public static $test_headers_written = array();

/**
* Options that will be used during unit testing.
*
* @internal
* @var []string
*/
public static $test_options = array();

/**
* Constructor.
*/
Expand All @@ -40,8 +93,10 @@ public function __construct() {
// Rewrite rendered content in REST API.
add_filter( 'the_content', array( self::class, 'rewrite_html' ), 100 );

// Resource hints. Note that the 'wp_head' is disabled for the time being due to CORS incompatibility.
// add_action( 'wp_head', array( self::class, 'add_head_tags' ), 0 ); .
/**
* Resource hints. Note that the 'wp_head' is disabled for the time being due to CORS incompatibility.
* add_action( 'wp_head', array( self::class, 'add_head_tags' ), 0 );
*/
add_action( 'send_headers', array( self::class, 'add_headers' ), 0 );

// REST API hooks.
Expand All @@ -61,21 +116,57 @@ public function __construct() {
add_filter( 'plugin_action_links_' . IMAGE_CDN_BASE, array( self::class, 'add_action_link' ) );
}

/**
* Outputs an HTTP header.
*
* @param string $key HTTP header key.
* @param string $value HTTP header value.
*/
private static function header( $key, $value ) {
$val = "$key: $value";
if ( self::$tests_running ) {
self::$test_headers_written[] = $val;
return;
}

header( $val );
}

/**
* Add http headers for Client Hints, Feature Policy and Preconnect Resource Hint.
*/
public static function add_headers() {
// Add client hints.
header( 'Accept-CH: viewport-width, width, device-memory, dpr, downlink, ect' );
self::header( 'Accept-CH', strtolower( implode( ', ', self::$client_hints ) ) );

// Add resource hints and feature policy.
$options = self::get_options();
$host = wp_parse_url( $options['url'], PHP_URL_HOST );
if ( ! empty( $host ) ) {
$protocol = ( is_ssl() ) ? 'https://' : 'http://';
header( 'Link: <' . $protocol . $host . '>; 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 ) ) );
}


Expand All @@ -84,7 +175,7 @@ public static function add_headers() {
*/
public static function add_head_tags() {
// Add client hints.
echo ' <meta http-equiv="Accept-CH" content="DPR, Viewport-Width, Width">' . "\n";
echo ' <meta http-equiv="Accept-CH" content="' . esc_attr( implode( ', ', self::$client_hints ) ) . '">' . "\n";

// Add resource hints.
$options = self::get_options();
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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() );
}

Expand Down
9 changes: 8 additions & 1 deletion imageengine/class-rewriter.php
Expand Up @@ -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;
}

Expand Down
26 changes: 24 additions & 2 deletions imageengine/class-settings.php
Expand Up @@ -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 );
Expand Down
5 changes: 5 additions & 0 deletions readme.txt
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions templates/settings.php
Expand Up @@ -18,11 +18,11 @@
);
?>
</p>
<p><?php esc_html_e( 'To obtain an ImageEngine Delivery Address' ); ?>:</p>
<p><?php esc_html_e( 'To obtain an ImageEngine Delivery Address:' ); ?></p>
<ol>
<li><a target="_blank" href="https://imageengine.io/signup/?website=<?php echo esc_attr( get_site_url() ); ?>&?utm_source=WP-plugin-settigns&utm_medium=page&utm_term=wp-imageengine&utm_campaign=wp-imageengine">Sign up for an ImageEngine account</a></li>
<li>
<?php
<?php
printf(
// translators: 1: http code example 2: https code example.
esc_html__( 'Enter the assigned ImageEngine Delivery Address (including %1$s or %2$s) in the "CDN URL" option below.', 'image-cdn' ),
Expand All @@ -34,7 +34,15 @@
</ol>
<p>See <a href="https://imageengine.io/docs/setup/quick-start/?utm_source=WP-plugin-settigns&utm_medium=page&utm_term=wp-imageengine&utm_campaign=wp-imageengine" target="_blank">full documentation.</a></p>
</div>
<h2><?php esc_html_e( 'Image CDN Settings', 'image-cdn' ); ?></h2>
<h2>
<?php
printf(
// translators: %s is the plugin version number.
'Image CDN Settings (version %s)',
esc_attr( IMAGE_CDN_VERSION )
)
?>
</h2>
<?php if ( $options['enabled'] && ! $is_runnable ) { ?>
<div class="notice notice-error">
<p>
Expand Down Expand Up @@ -119,11 +127,11 @@
<fieldset>
<label for="image_cdn_dirs">
<input type="text" name="image_cdn[dirs]" id="image_cdn_dirs" value="<?php echo esc_attr( $options['dirs'] ); ?>" size="64" class="regular-text code" />
<?php esc_html_e( 'Default', 'image-cdn' ); ?>: <code><?php echo esc_html( $defaults['dirs'] ); ?></code>
<?php esc_html_e( 'Optional; Default:', 'image-cdn' ); ?> <code><?php echo esc_html( $defaults['dirs'] ); ?></code>
</label>

<p class="description">
<?php esc_html_e( 'Assets in these directories will be pointed to the CDN URL. Enter the directories separated by', 'image-cdn' ); ?> <code>,</code>
<?php esc_html_e( 'Assets in these directories will be served by the CDN. Enter the directories separated by', 'image-cdn' ); ?> <code>,</code>
</p>
</fieldset>
</td>
Expand Down
8 changes: 8 additions & 0 deletions tests/standalone/bootstrap.php
Expand Up @@ -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;
}