<?php declare(strict_types=1);
namespace Weedesign\PageSpeed\Listener;
use Composer\Autoload\ClassLoader;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use JSMin\JSMin;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Weedesign\PageSpeed\Service\SaveHtaccessFile;
class ResponseListener
{
private $javascriptPlaceholder = '';
private $spacePlaceholder = ' ';
private $environment;
private SaveHtaccessFile $saveHtaccessFile;
private SystemConfigService $systemConfigService;
public function __construct(
string $environment,
SystemConfigService $systemConfigService,
SaveHtaccessFile $saveHtaccessFile
)
{
$this->systemConfigService = $systemConfigService;
$this->environment = $environment;
$this->saveHtaccessFile = $saveHtaccessFile;
}
/**
* @param ResponseEvent $event
*/
public function onKernelResponse(ResponseEvent $event): void
{
if($this->systemConfigService->get('WeedesignPageSpeed.config.prod')) {
if ($this->environment !== 'prod') {
return;
}
}
if (!$event->isMainRequest()) {
return;
}
$response = $event->getResponse();
if ($response instanceof BinaryFileResponse ||
$response instanceof StreamedResponse) {
return;
}
if ($response->getStatusCode() === Response::HTTP_NO_CONTENT) {
return;
}
if (strpos($response->headers->get('Content-Type', ''), 'text/html') === false) {
return;
}
$this->minify($response);
}
private function resetClassLoader(): void
{
$file = __DIR__.'/../../vendor/autoload.php';
if (!is_file($file)) {
return;
}
$classLoader = require_once $file;
if ($classLoader instanceof ClassLoader) {
$classLoader->unregister();
$classLoader->register(false);
}
}
private function minify(Response $response): void
{
$startTime = microtime(true);
$content = $response->getContent();
$lengthInitialContent = mb_strlen($content, 'utf8');
if ($lengthInitialContent === 0) {
return;
}
$htaccess = $this->saveHtaccessFile->save();
if ($this->systemConfigService->get('WeedesignPageSpeed.config.webp')) {
if ($this->systemConfigService->get('WeedesignPageSpeed.config.webpFrontend')) {
$this->systemConfigService->set('WeedesignPageSpeed.config.webpCount', 0);
} else {
$this->systemConfigService->set('WeedesignPageSpeed.config.webpCount', $this->systemConfigService->get('WeedesignPageSpeed.config.webpInt'));
}
}
if($this->systemConfigService->get('WeedesignPageSpeed.config.css')) {
if(!$this->systemConfigService->get('WeedesignPageSpeed.config.cssinline')) {
$this->minifySourceTypes($content);
$javascripts = $this->extractCombinedInlineScripts($content);
}
}
if($this->systemConfigService->get('WeedesignPageSpeed.config.html')) {
$this->minifyHtml($content);
}
if($this->systemConfigService->get('WeedesignPageSpeed.config.css')) {
if(!$this->systemConfigService->get('WeedesignPageSpeed.config.cssinline')) {
$content = str_replace($this->javascriptPlaceholder, '<script>' . $javascripts . '</script>', $content);
}
}
// $this->assignCompressionHeader($response, $content, $lengthInitialContent, $startTime);
$response->setContent($content);
}
private function minifyJavascript(string $content): string {
$this->resetClassLoader();
$jsMin = new JSMin($content);
return $jsMin->min();
}
private function minifyHtml(string &$content): void {
$search = [
'/(\n|^)(\x20+|\t)/',
'/(\n|^)\/\/(.*?)(\n|$)/',
'/\n/',
'/\<\!--.*?-->/',
'/(\x20+|\t)/', # Delete multispace (Without \n)
'/\s+\<label/', # keep whitespace before label tags
'/span\>\s+/', # keep whitespace after span tags
'/\s+\<span/', # keep whitespace before span tags
'/button\>\s+/', # keep whitespace after button tags
'/\s+\<button/', # keep whitespace before button tags
'/\>\s+\</', # strip whitespaces between tags
'/(\"|\')\s+\>/', # strip whitespaces between quotation ("') and end tags
'/=\s+(\"|\')/', # strip whitespaces between = "'
'/' . $this->spacePlaceholder . '/', # replace the spacePlaceholder at the end
];
$replace = [
"\n",
"\n",
' ',
'',
' ',
$this->spacePlaceholder . '<label',
'span>' . $this->spacePlaceholder,
$this->spacePlaceholder . '<span',
'button>' . $this->spacePlaceholder,
$this->spacePlaceholder . '<button',
'><',
'$1>',
'=$1',
' ',
];
$content = trim(preg_replace($search, $replace, $content));
}
private function extractCombinedInlineScripts(string &$content): string
{
$scriptContents = '';
$index = 0;
$placeholder = $this->javascriptPlaceholder;
if (strpos($content, '</script>') !== false) {
$content = preg_replace_callback('#<script>(.*?)<\/script>#s', function ($matches) use (&$scriptContents, &$index, $placeholder) {
$index++;
$content = trim($matches[1]);
if (!$this->str_ends_with($content, ';')) {
$content .= ';';
}
$scriptContents .= $content . PHP_EOL;
return $index === 1 ? $placeholder : '';
}, $content);
}
return $this->minifyJavascript($scriptContents);
}
private function minifySourceTypes(&$content): void
{
$search = [
'/ type=["\']text\/javascript["\']/',
'/ type=["\']text\/css["\']/',
];
$replace = '';
$content = preg_replace($search, $replace, $content);
}
private function assignCompressionHeader(Response $response, string $content, int $lengthInitialContent, float $startTime): void
{
$lengthContent = mb_strlen($content, 'utf8');
$savedData = round(100 - 100 / ($lengthInitialContent / $lengthContent), 2);
$timeTook = (int)((microtime(true) - $startTime) * 1000);
$response->headers->add(['X-Html-Compressor' => time() . ': ' . $savedData . '% ' . $timeTook . 'ms']);
}
private function str_ends_with(string $haystack, string $needle): bool {
return $needle === '' || substr_compare($haystack, $needle, -strlen($needle)) === 0;
}
}