View source
<?php
namespace Drupal\advagg\Asset;
use Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Component\Utility\Crypt;
abstract class AssetOptimizer {
protected abstract function fixType(array &$asset);
protected $extension;
protected $config;
protected $eventDispatcher;
protected $cache;
protected $cacheLevel;
protected $cacheTime;
protected $fixType;
protected $gZip;
protected $brotli;
protected $dnsPrefetch;
public function __construct(ConfigFactoryInterface $config_factory, ContainerAwareEventDispatcher $event_dispatcher, CacheBackendInterface $cache) {
$this->config = $config_factory
->get('advagg.settings');
$this->eventDispatcher = $event_dispatcher;
$this->cache = $cache;
$this->dnsPrefetch = [];
$this->cacheLevel = $this->config
->get('cache_level');
$this->fixType = $this->config
->get("{$this->extension}.fix_type");
if ($this->fixType) {
$this->basePath = substr($GLOBALS['base_root'] . $GLOBALS['base_path'], strpos($GLOBALS['base_root'] . $GLOBALS['base_path'], '//') + 2);
$this->basePathLen = strlen($this->basePath);
}
$this->gZip = $this
->shouldGZip();
$this->brotli = $this
->shouldBrotli();
}
public function processAssetArray(array &$assets) {
$protocol_relative = $this->config
->get('path.convert.absolute_to_protocol_relative');
$force_https = $this->config
->get('path.convert.force_https');
foreach ($assets as &$asset) {
if (!is_string($asset['data'])) {
continue;
}
if ($this->fixType) {
$this
->fixType($asset);
}
if ($asset['type'] === 'file' && $asset['preprocess']) {
if (!is_readable($asset['data'])) {
continue;
}
$this
->scanFile($asset);
}
elseif ($asset['type'] === 'external') {
if ($force_https) {
$asset['data'] = $this
->convertPathForceHttps($asset['data']);
}
elseif ($protocol_relative) {
$asset['data'] = $this
->convertPathProtocolRelative($asset['data']);
}
$scheme = parse_url($asset['data'], PHP_URL_SCHEME);
$host = parse_url($asset['data'], PHP_URL_HOST);
$asset_url = isset($scheme) ? "{$scheme}://{$host}" : "//{$host}";
$this->dnsPrefetch[] = $asset_url;
}
}
if (!isset($GLOBALS['_advagg_prefetch'])) {
$GLOBALS['_advagg_prefetch'] = [];
}
$GLOBALS['_advagg_prefetch'] += $this->dnsPrefetch;
}
protected function scanFile(array &$asset) {
clearstatcache(TRUE, $asset['data']);
$cid = Crypt::hashBase64($asset['data'] . $this->config
->get('global_counter'));
$cached = $this->cache
->get($cid);
if ($cached && file_exists($cached->data['file'])) {
if ($this->config
->get('css.combine_media') && isset($asset['media']) && $asset['media'] !== 'all') {
$asset['media'] = 'all';
}
$asset['size'] = $cached->data['filesize'];
if ($this->cacheLevel === 3) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
$data = [
'filesize' => (int) @filesize($asset['data']),
'mtime' => @filemtime($asset['data']),
];
if ($this->cacheLevel === 2) {
if ($cached->data['mtime'] === $data['mtime']) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
}
$data['contents'] = @file_get_contents($asset['data']);
$data['hash'] = Crypt::hashBase64($data['contents']);
if ($this->cacheLevel === 1) {
if ($cached->data['hash'] === $data['hash']) {
$asset['data'] = $cached->data['file'];
$this->dnsPrefetch += $cached->data['prefetch'];
return;
}
}
}
if (empty($data)) {
$data = [
'filesize' => (int) @filesize($asset['data']),
'mtime' => @filemtime($asset['data']),
'contents' => @file_get_contents($asset['data']),
];
$data['hash'] = Crypt::hashBase64($data['contents']);
}
$data['cid'] = $cid;
$asset['size'] = $data['filesize'];
if ($data['file'] = $this
->optimizeFile($asset, $data)) {
$asset['contents'] = $data['contents'];
$data['prefetch'] = $this
->addDnsPrefetch($asset);
$this->dnsPrefetch += $data['prefetch'];
$data['original'] = $asset['data'];
unset($data['contents']);
unset($data['cid']);
$this->cache
->set($cid, $data, $this
->getCacheTime(), [
'advagg',
]);
$asset['data'] = $data['file'];
}
}
protected function writeFile($data, $cid) {
$path = 'public://' . $this->extension . '/optimized';
$version = Crypt::hashBase64($data);
$uri = "{$path}/{$this->extension}_{$cid}.{$version}.{$this->extension}";
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
if (!file_exists($uri)) {
if (!file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
return FALSE;
}
}
if ($this->gZip && !file_exists($uri . '.gz')) {
file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE);
}
if ($this->brotli && !file_exists($uri . '.br')) {
file_unmanaged_save_data(brotli_compress($data, 11, BROTLI_TEXT), $uri . '.br', FILE_EXISTS_REPLACE);
}
return $uri;
}
protected function shouldGZip() {
if (extension_loaded('zlib') && \Drupal::config('system.performance')
->get($this->extension . '.gzip')) {
return TRUE;
}
return FALSE;
}
protected function shouldBrotli() {
if (function_exists('brotli_compress') && $this->config
->get($this->extension . '.brotli')) {
return TRUE;
}
return FALSE;
}
protected function getCacheTime() {
if ($this->cacheTime) {
return $this->cacheTime;
}
$this->cacheTime = (int) microtime(TRUE);
if ($this->cacheLevel === 1) {
$this->cacheTime += 86400;
}
elseif ($this->cacheLevel === 2) {
$this->cacheTime += 604800;
}
elseif ($this->cacheLevel === 3) {
$this->cacheTime += 2419200;
}
return $this->cacheTime;
}
protected abstract function optimizeFile(array &$asset, array $data);
protected abstract function addDnsPrefetch(array $asset);
protected function convertPathProtocolRelative($path) {
if (strpos($path, 'https://') === 0) {
$path = substr($path, 6);
}
elseif (strpos($path, 'http://') === 0) {
$path = substr($path, 5);
}
return $path;
}
protected function convertPathForceHttps($path) {
if (strpos($path, 'http://') === 0) {
$path = 'https://' . substr($path, 7);
}
return $path;
}
public static function sortStable(array &$assets) {
$nested = [];
foreach ($assets as $key => $item) {
$weight = (string) $item['weight'];
$nested[$item['group']][$weight][$key] = $item;
}
$sorted = [];
ksort($nested);
foreach ($nested as &$group_items) {
ksort($group_items);
foreach ($group_items as &$weight_items) {
foreach ($weight_items as $key => &$item) {
$sorted[$key] = $item;
}
unset($item);
}
unset($weight_items);
}
unset($group_items);
$assets = $sorted;
}
public static function generateHtaccess($extension, $regenerate = FALSE) {
$path = "public://{$extension}/optimized";
$file = $path . '/.htaccess';
if (!$regenerate && file_exists($file)) {
return;
}
$config = \Drupal::config('advagg.settings');
if ($extension === 'js') {
$type = 'application/javascript';
}
else {
$type = 'text/css';
}
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
$immutable = $config
->get('immutable') ? ', immutable' : '';
$options = '';
if ($config
->get('symlinks')) {
$options = 'Options +FollowSymlinks';
}
elseif ($config
->get('symlinksifownermatch')) {
$options = 'Options +SymLinksIfOwnerMatch';
}
$htaccess = <<<EOT
{<span class="php-variable">$options</span>}
<IfModule mod_rewrite.c>
RewriteEngine on
<IfModule mod_headers.c>
# Serve brotli compressed {<span class="php-variable">$extension</span>} files if they exist and the client accepts br.
RewriteCond %{HTTP:Accept-encoding} br
RewriteCond %{REQUEST_FILENAME}\\.br -s
RewriteRule ^(.*)\\.{<span class="php-variable">$extension</span>} \$1\\.{<span class="php-variable">$extension</span>}\\.br [QSA]
RewriteRule \\.{<span class="php-variable">$extension</span>}\\.br\$ - [T={<span class="php-variable">$type</span>},E=no-gzip:1]
<FilesMatch "\\.{<span class="php-variable">$extension</span>}\\.br\$">
# Serve correct encoding type.
Header set Content-Encoding br
# Force proxies to cache br/gzip/non-gzipped assets separately.
Header append Vary Accept-Encoding
</FilesMatch>
# Serve gzip compressed {<span class="php-variable">$extension</span>} files if they exist and the client accepts gzip.
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\\.gz -s
RewriteRule ^(.*)\\.{<span class="php-variable">$extension</span>} \$1\\.{<span class="php-variable">$extension</span>}\\.gz [QSA]
RewriteRule \\.{<span class="php-variable">$extension</span>}\\.gz\$ - [T=application/javascript,E=no-gzip:1]
<FilesMatch "\\.{<span class="php-variable">$extension</span>}\\.gz\$">
# Serve correct encoding type.
Header set Content-Encoding gzip
# Force proxies to cache br/gzip/non-gzipped assets separately.
Header append Vary Accept-Encoding
</FilesMatch>
</IfModule>
</IfModule>
<FilesMatch "{<span class="php-variable">$extension</span>}(\\.gz|\\.br)?">
# No mod_headers. Apache module headers is not enabled.
<IfModule !mod_headers.c>
# No mod_expires. Apache module expires is not enabled.
<IfModule !mod_expires.c>
# Use ETags.
FileETag MTime Size
</IfModule>
</IfModule>
# Use Expires Directive if apache module expires is enabled.
<IfModule mod_expires.c>
# Do not use ETags.
FileETag None
# Enable expirations.
ExpiresActive On
# Cache all aggregated {<span class="php-variable">$extension</span>} files for 52 weeks after access (A).
ExpiresDefault A31449600
</IfModule>
# Use Headers Directive if apache module headers is enabled.
<IfModule mod_headers.c>
# Do not use etags for cache validation.
Header unset ETag
# Serve correct content type.
Header set Content-Type {<span class="php-variable">$type</span>}
<IfModule !mod_expires.c>
# Set a far future Cache-Control header to 52 weeks.
Header set Cache-Control "max-age=31449600, no-transform, public{<span class="php-variable">$immutable</span>}"
</IfModule>
<IfModule mod_expires.c>
Header append Cache-Control "no-transform, public{<span class="php-variable">$immutable</span>}"
</IfModule>
</IfModule>
ForceType {<span class="php-variable">$type</span>}
</FilesMatch>
EOT;
file_unmanaged_save_data($htaccess, $file, FILE_EXISTS_REPLACE);
}
}