*/
class Abovethefold_PWA
{
/**
* Above the fold controller
*/
public $CTRL;
// preload list
private $preload = array();
/**
* Initialize the class and set its properties
*/
public function __construct(&$CTRL)
{
$this->CTRL = & $CTRL;
if (isset($this->CTRL->options['pwa_cache_preload']) && $this->CTRL->options['pwa_cache_preload']) {
$this->preload = $this->CTRL->options['pwa_cache_preload'];
}
if ($this->CTRL->disabled) {
return; // above the fold optimization disabled for area / page
}
}
/**
* Return service worker path
*/
public function get_sw()
{
$path = trailingslashit(ABSPATH);
$sw_filename = 'abtf-pwa.js';
$sw_filename_debug = 'abtf-pwa.debug.js';
$sw_config_filename = 'abtf-pwa-config.json';
return array(
'filename' => $sw_filename,
'filename_debug' => $sw_filename_debug,
'filename_config' => $sw_config_filename,
'file' => $path . $sw_filename,
'file_debug' => $path . $sw_filename_debug,
'file_config' => $path . $sw_config_filename
);
}
/**
* Return service worker scope
*/
public function get_sw_scope()
{
if (isset($this->CTRL->options['pwa_scope']) && trim($this->CTRL->options['pwa_scope']) !== '') {
$scope = $this->CTRL->options['pwa_scope'];
} else {
$scope = trailingslashit(parse_url(site_url(), PHP_URL_PATH));
}
return apply_filters('abtf_pwa_sw_scope', $scope);
}
/**
* Return service worker path
*/
public function get_sw_path($debug = false)
{
$sw = $this->get_sw();
$file = ($debug) ? $sw['filename_debug'] : $sw['filename'];
$path = trailingslashit(parse_url(site_url(), PHP_URL_PATH));
return apply_filters('abtf_pwa_sw_path', $path . $file . '?path=' . urlencode($path));
}
/**
* Get Service Worker config
*/
public function get_sw_config()
{
$cache_policy = array();
// asset cache
if (isset($this->CTRL->options['pwa_cache_assets']) && $this->CTRL->options['pwa_cache_assets'] && is_array($this->CTRL->options['pwa_cache_assets_policy'])) {
$cache_policy = $this->CTRL->options['pwa_cache_assets_policy'];
} else {
$cache_policy = array(); // $this->get_sw_default_policy();
}
// page cache
if (isset($this->CTRL->options['pwa_cache_pages']) && $this->CTRL->options['pwa_cache_pages']) {
// create page cache policy
$page_cache_policy = array(
'title' => 'Match pages',
'match' => array(
array( 'type' => 'header', 'name' => 'Accept', 'pattern' => 'text/html')
),
'strategy' => $this->CTRL->options['pwa_cache_pages_strategy'],
'cache' => array(
'conditions' => array(
array(
'type' => 'header',
'name' => 'content-type',
'pattern' => 'text/html'
)
)
)
);
// add URL match based on include list
if (isset($this->CTRL->options['pwa_cache_pages_include']) && $this->CTRL->options['pwa_cache_pages_include']) {
$page_cache_policy['match'][] = array(
'type' => 'url', 'pattern' => $this->CTRL->options['pwa_cache_pages_include']
);
}
// offline page
if (isset($this->CTRL->options['pwa_cache_pages_offline']) && $this->CTRL->options['pwa_cache_pages_offline']) {
$page_cache_policy['offline'] = $this->CTRL->options['pwa_cache_pages_offline'];
}
// cache strategy
if ($this->CTRL->options['pwa_cache_pages_strategy'] === 'cache') {
if (!isset($page_cache_policy['cache'])) {
$page_cache_policy['cache'] = array();
}
if (isset($this->CTRL->options['pwa_cache_pages_update_interval']) && $this->CTRL->options['pwa_cache_pages_update_interval']) {
$page_cache_policy['cache']['update_interval'] = intval($this->CTRL->options['pwa_cache_pages_update_interval']);
}
if (isset($this->CTRL->options['pwa_cache_pages_max_age']) && $this->CTRL->options['pwa_cache_pages_max_age']) {
$page_cache_policy['cache']['max_age'] = intval($this->CTRL->options['pwa_cache_pages_max_age']);
}
$page_cache_policy['cache']['head_update'] = (isset($this->CTRL->options['pwa_cache_pages_head_update']) && $this->CTRL->options['pwa_cache_pages_head_update']) ? true : false;
$page_cache_policy['cache']['notify'] = (isset($this->CTRL->options['pwa_cache_pages_update_notify']) && $this->CTRL->options['pwa_cache_pages_update_notify']) ? true : false;
}
$cache_policy[] = $page_cache_policy;
// Lighthouse audit bug results in false negative, require URL based match of start URL
// @link https://github.com/GoogleChrome/lighthouse/issues/4312
if (isset($this->CTRL->options['pwa_manifest_start_url']) && $this->CTRL->options['pwa_manifest_start_url'] && $this->CTRL->options['pwa_manifest_start_url'] !== '/') {
// create start url cache policy
$page_cache_policy['match'] = array(
array( 'type' => 'url', 'pattern' => $this->CTRL->options['pwa_manifest_start_url'] )
);
$cache_policy[] = $page_cache_policy;
}
}
$config = array(
'policy' => $cache_policy
);
// preload assets
// apply filters
$this->preload = apply_filters('abtf_pwa_preload', $this->preload);
if (!empty($this->preload)) {
$config['preload'] = $this->preload;
$config['preload_install'] = (isset($this->CTRL->options['pwa_cache_preload_require']) && $this->CTRL->options['pwa_cache_preload_require']) ? true : false;
}
if (isset($this->CTRL->options['pwa_manifest_start_url']) && $this->CTRL->options['pwa_manifest_start_url']) {
$config['start_url'] = $this->CTRL->options['pwa_manifest_start_url'];
}
if (isset($this->CTRL->options['pwa_cache_version']) && $this->CTRL->options['pwa_cache_version']) {
$config['cache_version'] = $this->CTRL->options['pwa_cache_version'];
}
return $config;
}
/**
* Update Service Worker file
*/
public function update_sw()
{
$sw_ok = false;
$sw = $this->get_sw();
$sources = array(
'pwa-serviceworker.js' => $sw['file'],
'pwa-serviceworker.debug.js' => $sw['file_debug']
);
foreach ($sources as $sourcefile => $sw_path) {
$source = trailingslashit(WPABTF_PATH) . 'public/js/' . $sourcefile;
if (!file_exists($source)) {
$this->CTRL->admin->set_notice('The service worker source file (above-the-fold-optimization/public/js/'.$sourcefile.') is missing.', 'ERROR');
} else {
$sw_ok = true;
if (!file_exists($sw_path) || md5_file($source) !== md5_file($sw_path)) {
try {
@file_put_contents($sw_path, file_get_contents($source));
} catch (Exception $error) {
$sw_ok = false;
}
if (!file_exists($sw_path)) {
$sw_ok = false;
} elseif ($sw_ok && md5_file($source) !== md5_file($sw_path)) {
$sw_ok = false;
}
}
if (!$sw_ok) {
if (isset($this->CTRL->admin)) {
$this->CTRL->admin->set_notice('Failed to install the Service Worker on ' . esc_html(str_replace(ABSPATH, '[ABSPATH]/', $sw_path)) . '. Please check the permissions or copy the file manually from ' . esc_html(str_replace(ABSPATH, '[ABSPATH]/', trailingslashit(WPABTF_PATH) . 'public/js/'.$sourcefile)) . ' (download).', 'ERROR');
}
}
}
}
if (!$sw_ok) {
return false;
}
return true;
}
/**
* Update Service Worker config
*/
public function update_sw_config()
{
$sw = $this->get_sw();
$config = $this->get_sw_config();
$config_json = json_encode($config);
$sw_config_ok = true;
$current_config = (file_exists($sw['file_config'])) ? file_get_contents($sw['file_config']) : false;
if (!$current_config || md5($current_config) !== md5($config_json)) {
try {
@file_put_contents($sw['file_config'], $config_json);
} catch (Exception $error) {
$sw_config_ok = false;
}
if (!file_exists($sw['file_config'])) {
$sw_config_ok = false;
} elseif ($sw_config_ok && md5(file_get_contents($sw['file_config'])) !== md5($config_json)) {
$sw_config_ok = false;
}
}
if (!$sw_config_ok) {
return false;
}
return true;
}
/**
* Return default asset cache policy
*/
public function get_sw_default_policy()
{
// default cache policy
return array(
array(
'title' => 'Match images',
'match' => array(
array(
'type' => 'header',
'name' => 'Accept',
'pattern' => 'image/'
),
array(
'not' => true,
'type' => 'header',
'name' => 'Accept',
'pattern' => 'text/html'
),
array(
"not" => true,
"type" => "url",
"pattern" => "google-analytics.com/collect"
)
),
'strategy' => 'cache',
'cache' => array(
'update_interval' => 3600,
'head_update' => true,
'conditions' => array(
array(
'type' => 'header',
'name' => 'content-length',
'pattern' => array( 'operator' => '<', 'value' => 35840 )
)
)
),
'offline' => '/path/to/offline.png'
),
array(
'title' => 'Match assets',
'match' => array(
array(
'type' => 'url',
'pattern' => '/\.(css|js|woff|woff2|ttf|otf|eot)(\?.*)?$/i',
'regex' => true
)
),
'strategy' => 'cache',
'cache' => array(
'update_interval' => 300,
'head_update' => true,
'max_age' => 86400
)
)
);
}
/**
* Javascript client settings
*/
public function client_jssettings(&$jssettings, &$jsfiles, &$inlineJS, $jsdebug, &$html_before)
{
// print link to manifest.json
if (isset($this->CTRL->options['pwa_manifest_meta']) && $this->CTRL->options['pwa_manifest_meta']) {
$html_before .= '';
}
// print Web App meta
if (isset($this->CTRL->options['pwa_meta']) && $this->CTRL->options['pwa_meta']) {
$html_before .= $this->CTRL->options['pwa_meta'];
}
// PWA client
if (!isset($this->CTRL->options['pwa']) || !$this->CTRL->options['pwa']) {
if (isset($this->CTRL->options['pwa_unregister']) && $this->CTRL->options['pwa_unregister']) {
// unregister
$jssettings[$this->CTRL->optimization->client_config_ref['pwa_unregister']] = true;
$jsfiles[] = WPABTF_PATH . 'public/js/abovethefold-pwa-unregister'.$jsdebug.'.min.js';
}
// disabled
return;
}
// get service worker paths
$sw = $this->get_sw();
// verify if service worker file exist
$swfile = ($jsdebug) ? $sw['filename_debug'] : $sw['filename'];
if (!file_exists($swfile)) {
// debug file missing, fallback to regular
if ($jsdebug && file_exists($sw['filename'])) {
$swfile = $sw['filename'];
} else {
// disable
return;
}
}
// no config file
if (!file_exists($sw['file_config'])) {
// disable
return;
}
// config index key
$pwaindex = $this->CTRL->optimization->client_config_ref['pwa'];
// client settings
$pwasettings = array(
'path' => $this->get_sw_path($jsdebug),
'scope' => $this->get_sw_scope(),
'policy' => filemtime($sw['file_config']),
'register' => (!isset($this->CTRL->options['pwa_register']) || $this->CTRL->options['pwa_register'])
);
// offline class
if (isset($this->CTRL->options['pwa_offline_class']) && $this->CTRL->options['pwa_offline_class']) {
$pwasettings['offline_class'] = true;
}
// version
if (isset($this->CTRL->options['pwa_cache_version']) && $this->CTRL->options['pwa_cache_version'] !== '') {
$pwasettings['version'] = $this->CTRL->options['pwa_cache_version'];
} else {
$pwasettings['version'] = '';
}
// version
if (isset($this->CTRL->options['pwa_cache_max_size']) && $this->CTRL->options['pwa_cache_max_size'] !== '') {
$pwasettings['max_size'] = $this->CTRL->options['pwa_cache_max_size'];
} else {
$pwasettings['max_size'] = '';
}
// preload on mouse down
if (isset($this->CTRL->options['pwa_preload_mousedown']) && $this->CTRL->options['pwa_preload_mousedown']) {
$pwasettings['preload_mousedown'] = true;
} else {
$pwasettings['preload_mousedown'] = false;
}
// add pwa settings to client settings
$jssettings[$pwaindex] = array();
foreach ($pwasettings as $key => $value) {
if (!isset($this->CTRL->optimization->client_config_ref['pwa-sub'][$key])) {
continue;
}
$jssettings[$pwaindex][$this->CTRL->optimization->client_config_ref['pwa-sub'][$key]] = $value;
}
// fill empty array values to preserve JSON array format
$max = 0;
foreach ($jssettings[$pwaindex] as $index => $value) {
if ($index > $max) {
$max = $index;
}
}
if ($max > 0) {
for ($i = 0; $i <= $max; $i++) {
if (!isset($jssettings[$pwaindex][$i])) {
$jssettings[$pwaindex][$i] = -1;
}
}
ksort($jssettings[$pwaindex]);
}
$jsfiles[] = WPABTF_PATH . 'public/js/abovethefold-pwa'.$jsdebug.'.min.js';
}
}