*/
class Abovethefold_HTTP2
{
/**
* Above the fold controller
*/
public $CTRL;
public $push = false;
public $config;
public $enqueued = array();
/**
* Initialize the class and set its properties
*/
public function __construct(&$CTRL)
{
$this->CTRL = & $CTRL;
if ($this->CTRL->disabled) {
return; // above the fold optimization disabled for area / page
}
// HTTP/2 optimization enabled
if (isset($this->CTRL->options['http2_push']) && $this->CTRL->options['http2_push']) {
$this->push = (isset($this->CTRL->options['http2_push_config']) && is_array($this->CTRL->options['http2_push_config'])) ? $this->CTRL->options['http2_push_config'] : array();
}
if (empty($this->push)) {
$this->push = false;
}
if (!$this->push && isset($this->CTRL->options['http2_push_criticalcss']) && $this->CTRL->options['http2_push_criticalcss']) {
$this->push = array();
}
// HTTP/2 Server Push enabled
if (is_array($this->push)) {
// add headers
$this->CTRL->loader->add_action('abtf_html_pre', $this, 'push_headers', 10);
}
}
/**
* Output HTTP/2 push headers
*/
public function push_headers($buffer)
{
// push HTML images?
$pushTypes = array();
foreach ($this->push as $push) {
if (!isset($pushTypes[$push['type']])) {
$pushTypes[$push['type']] = array();
}
$pushTypes[$push['type']][] = $push;
}
// resources to push
$pushResources = array();
// first priority: Critical CSS
if ($this->CTRL->optimization->http2push_criticalcss) {
$local = $this->is_local($this->CTRL->optimization->http2push_criticalcss);
$pushResources[] = array(
'src' => esc_url((($local) ? $this->url_to_relative_path($this->CTRL->optimization->http2push_criticalcss) : $this->CTRL->optimization->http2push_criticalcss)),
'local' => $local,
'type' => 'style',
'meta' => true
);
}
// WordPress stylesheets
if (isset($pushTypes['style'])) {
global $wp_styles;
foreach ($wp_styles->queue as $style) {
$src = $wp_styles->registered[$style]->src;
$match = $meta = false;
foreach ($pushTypes['style'] as $rule) {
$this->matchRule($src, $rule, $match, $meta);
}
if ($match) {
$local = $this->is_local($src); // (strpos($src, home_url()) !== false);
$pushResources[] = array(
'src' => esc_url((($local) ? $this->url_to_relative_path($src) : $src)),
'local' => $local,
'type' => 'style',
'meta' => $meta
);
}
}
}
// WordPress scripts
if (isset($pushTypes['script'])) {
global $wp_scripts;
foreach ($wp_scripts->queue as $script) {
$src = $wp_scripts->registered[$script]->src;
$match = $meta = false;
foreach ($pushTypes['script'] as $rule) {
$this->matchRule($src, $rule, $match, $meta);
}
if ($match) {
$local = $this->is_local($src);
$pushResources[] = array(
'src' => esc_url((($local) ? $this->url_to_relative_path($src) : $src)),
'local' => $local,
'type' => 'script',
'meta' => $meta
);
}
}
}
// HTML images
if (isset($pushTypes['image'])) {
// image regex
$image_regex = '#
]+src[^>]+>#is';
if (preg_match_all($image_regex, $buffer, $out)) {
foreach ($out[0] as $n => $image) {
// extract image
$src = $this->src_regex($out[0][$n]);
if (!$src) {
continue 1;
}
// not a valid URL / path
if (strpos($src, 'http') === 0 || strpos($src, '/') === 0) {
$images[] = $src;
}
}
$images = array_unique($images);
foreach ($images as $src) {
$match = $meta = false;
foreach ($pushTypes['image'] as $rule) {
$this->matchRule($src, $rule, $match, $meta);
}
if ($match) {
$local = $this->is_local($src);
$pushResources[] = array(
'src' => esc_url((($local) ? $this->url_to_relative_path($src) : $src)),
'local' => $local,
'type' => 'image',
'meta' => $meta
);
}
}
}
}
// custom resource list
if (isset($pushTypes['custom'])) {
foreach ($pushTypes['custom'] as $push) {
if (isset($push['resources']) && !empty($push['resources'])) {
foreach ($push['resources'] as $resource) {
$local = $this->is_local($resource['file']);
$src = $resource['file'];
$pushResources[] = array(
'src' => esc_url((($local) ? $this->url_to_relative_path($src) : $src)),
'local' => $local,
'type' => $resource['type'],
'mime' => (isset($resource['mime']) && $resource['mime']) ? $resource['mime'] : false,
'meta' => (isset($push['meta']) && $push['meta']) ? $push['meta'] : false
);
}
}
}
}
// create HTTP/2 Push Header
$links = $meta = array();
foreach ($pushResources as $resource) {
$link = sprintf(
'<%s>; rel=preload; as=%s',
$resource['src'],
sanitize_html_class($resource['type'])
);
if (!$resource['local']) {
$link .= '; crossorigin';
}
if (isset($resource['mime']) && $resource['mime']) {
$link .= sprintf('; type=\'%s\'', str_replace('\'', '\\\'', $resource['mime']));
}
// CloudFlare support for ServerPush appears to be broken as of Sept. 8, 2017
// @link https://community.cloudflare.com/t/is-http-2-server-push-disabled/5577
//
// awaiting the status of the availability and exact requirement for the Link: header,
// the 1 link per header implementation is used from the official CloudFlare plugin,
// instead of a more compact grouped header
header('Link: ' . $link, false);
//$links[] = $link;
// meta tag fallback
if ($resource['meta']) {
$link = sprintf(
'';
}
}
// output HTTP/2 Push Header
//header('Link: ' . implode(',',$links), false);
// include fallback meta
$buffer = str_replace('', implode('', $meta), $buffer);
return $buffer;
}
/**
* Extract src from tag
*/
public function src_regex($tag)
{
// detect if tag has href
$srcpos = strpos($tag, 'src');
if ($srcpos !== false) {
// regex
$char = substr($tag, ($srcpos + 4), 1);
if ($char === '"' || $char === '\'') {
$char = preg_quote($char);
$regex = '#(src\s*=\s*'.$char.')([^'.$char.']+)('.$char.')#Usmi';
} elseif ($char === ' ' || $char === "\n") {
$regex = '#(src\s*=\s*["|\'])([^"|\']+)(["|\'])#Usmi';
} else {
$regex = '#(src\s*=)([^\s]+)(\s)#Usmi';
}
// match href
if (!preg_match($regex, $tag, $out)) {
return false;
}
return $out[2];
}
return false;
}
/**
* Match resource push rule
*/
public function matchRule($src, $rule, &$match, &$meta)
{
if ($rule['match'] === 'all') {
$match = true;
if (isset($rule['meta'])) {
$meta = $rule['meta'];
}
} elseif (isset($rule['match']) && is_array($rule['match']) && isset($rule['match']['pattern'])) {
$regex = (isset($rule['match']['regex']) && $rule['match']['regex']);
$exclude = (isset($rule['match']['exclude']) && $rule['match']['exclude']);
if ($exclude && !$match) {
// not included
} else {
if ($regex) {
if (!@preg_match($rule['match']['pattern'], $src)) {
// no match
} else {
// exclude resource
if ($exclude) {
$match = false;
} else {
$match = true;
if (isset($rule['meta'])) {
$meta = $rule['meta'];
}
}
}
} else {
if (strpos($src, $rule['match']['pattern']) !== false) {
// exclude resource
if ($exclude) {
$match = false;
} else {
$match = true;
if (isset($rule['meta'])) {
$meta = $rule['meta'];
}
}
}
}
}
}
return array($match,$meta);
}
/**
* Local resource?
*/
public function is_local($src)
{
// relative path
if (strpos($src, '://') === false && strpos($src, '//') !== 0) {
return true;
}
return !!$this->CTRL->proxy->is_local($src);
}
/**
* URL to relative path
*/
public function url_to_relative_path($src)
{
return '//' === substr($src, 0, 2) ? preg_replace('/^\/\/([^\/]*)\//', '/', $src) : preg_replace('/^http(s)?:\/\/[^\/]*/', '', $src);
}
/**
* Return enqueue type
*/
public static function enqueue_type($current_hook)
{
return 'style_loader_src' === $current_hook ? 'style' : 'script';
}
/**
* Javascript client settings
*/
public function client_jssettings(&$html_after)
{
// HTTP/2 optimization enabled
if (!$this->push) {
// disabled
return;
}
$html_after .= '';
}
}