| 1 |
<?php |
| 2 |
|
| 3 |
/** |
| 4 |
* JCH Optimize - Performs several front-end optimizations for fast downloads |
| 5 |
* |
| 6 |
* @package jchoptimize/core |
| 7 |
* @author Samuel Marshall <samuel@jch-optimize.net> |
| 8 |
* @copyright Copyright (c) 2022 Samuel Marshall / JCH Optimize |
| 9 |
* @license GNU/GPLv3, or later. See LICENSE file |
| 10 |
* |
| 11 |
* If LICENSE file missing, see <http://www.gnu.org/licenses/>. |
| 12 |
*/ |
| 13 |
|
| 14 |
namespace JchOptimize\Core; |
| 15 |
|
| 16 |
use _JchOptimizeVendor\V91\Joomla\Input\Input; |
| 17 |
use _JchOptimizeVendor\V91\Laminas\Cache\Exception\ExceptionInterface; |
| 18 |
use _JchOptimizeVendor\V91\Laminas\Cache\Storage\StorageInterface; |
| 19 |
use DateInterval; |
| 20 |
use DateTime; |
| 21 |
use JchOptimize\Container\ContainerFactory; |
| 22 |
|
| 23 |
use function apache_request_headers; |
| 24 |
use function array_intersect; |
| 25 |
use function array_keys; |
| 26 |
use function array_map; |
| 27 |
use function defined; |
| 28 |
use function explode; |
| 29 |
use function function_exists; |
| 30 |
use function gzencode; |
| 31 |
use function header; |
| 32 |
use function is_null; |
| 33 |
use function preg_match; |
| 34 |
use function strtolower; |
| 35 |
use function strtotime; |
| 36 |
|
| 37 |
defined('_JCH_EXEC') or die('Restricted access'); |
| 38 |
|
| 39 |
class Output |
| 40 |
{ |
| 41 |
/** |
| 42 |
* @param array $vars |
| 43 |
* @param bool $bSend True to output to browser otherwise return result |
| 44 |
* |
| 45 |
* @return bool|string|string[]|void |
| 46 |
*/ |
| 47 |
public static function getCombinedFile(array $vars = [], bool $bSend = true) |
| 48 |
{ |
| 49 |
$container = ContainerFactory::getInstance(); |
| 50 |
|
| 51 |
/** @var StorageInterface $cache */ |
| 52 |
$cache = $container->get(StorageInterface::class); |
| 53 |
|
| 54 |
$input = new Input(); |
| 55 |
|
| 56 |
if (empty($vars)) { |
| 57 |
$vars = [ |
| 58 |
'f' => $input->getBase64('f'), |
| 59 |
'type' => $input->getWord('type'), |
| 60 |
'gz' => $input->getWord('gz', 'nz') |
| 61 |
]; |
| 62 |
} |
| 63 |
|
| 64 |
try { |
| 65 |
//Temporarily set lifetime to 0 and fetch cache |
| 66 |
$lifetime = $cache->getOptions()->getTtl(); |
| 67 |
$cache->getOptions()->setTtl(0); |
| 68 |
|
| 69 |
/** @var CacheObject[] $resultsObj */ |
| 70 |
$resultsObj = $cache->getItem($vars['f']); |
| 71 |
|
| 72 |
$cache->getOptions()->setTtl($lifetime); |
| 73 |
} catch (ExceptionInterface $e) { |
| 74 |
$resultsObj = null; |
| 75 |
} |
| 76 |
|
| 77 |
if (is_null($resultsObj)) { |
| 78 |
if ($bSend) { |
| 79 |
header("HTTP/1.0 404 Not Found"); |
| 80 |
|
| 81 |
echo 'File not found'; |
| 82 |
} |
| 83 |
|
| 84 |
return false; |
| 85 |
} |
| 86 |
|
| 87 |
$file = $resultsObj[0]->getContents(); |
| 88 |
|
| 89 |
//Return file if we're not outputting to browser |
| 90 |
if (!$bSend) { |
| 91 |
return $file; |
| 92 |
} |
| 93 |
|
| 94 |
$aTimeMFile = self::RFC1123DateAdd($resultsObj[0]->getFilemtime(), '1 year'); |
| 95 |
|
| 96 |
$timeMFile = $aTimeMFile['filemtime'] . ' GMT'; |
| 97 |
$expiryDate = $aTimeMFile['expiry'] . ' GMT'; |
| 98 |
|
| 99 |
$modifiedSinceTime = ''; |
| 100 |
$noneMatch = ''; |
| 101 |
|
| 102 |
if (function_exists('apache_request_headers')) { |
| 103 |
$headers = apache_request_headers(); |
| 104 |
|
| 105 |
if (isset($headers['If-Modified-Since'])) { |
| 106 |
$modifiedSinceTime = strtotime($headers['If-Modified-Since']); |
| 107 |
} |
| 108 |
|
| 109 |
if (isset($headers['If-None-Match'])) { |
| 110 |
$noneMatch = $headers['If-None-Match']; |
| 111 |
} |
| 112 |
} |
| 113 |
|
| 114 |
if ($modifiedSinceTime == '' && !is_null($input->server->getString('HTTP_IF_MODIFIED_SINCE'))) { |
| 115 |
$modifiedSinceTime = strtotime($input->server->getString('HTTP_IF_MODIFIED_SINCE')); |
| 116 |
} |
| 117 |
|
| 118 |
if ($noneMatch == '' && !is_null($input->server->getString('HTTP_IF_NONE_MATCH'))) { |
| 119 |
$noneMatch = $input->server->getString('HTTP_IF_NONE_MATCH'); |
| 120 |
} |
| 121 |
|
| 122 |
$etag = $resultsObj[0]->getEtag(); |
| 123 |
|
| 124 |
if ($modifiedSinceTime == strtotime($timeMFile) || trim($noneMatch) == $etag) { |
| 125 |
// Client's cache IS current, so we just respond '304 Not Modified'. |
| 126 |
header('HTTP/1.1 304 Not Modified'); |
| 127 |
header('Content-Length: 0'); |
| 128 |
|
| 129 |
return; |
| 130 |
} else { |
| 131 |
header('Last-Modified: ' . $timeMFile); |
| 132 |
} |
| 133 |
|
| 134 |
|
| 135 |
if ($vars['type'] == 'css') { |
| 136 |
header('Content-type: text/css'); |
| 137 |
} elseif ($vars['type'] == 'js') { |
| 138 |
header('Content-type: text/javascript'); |
| 139 |
} |
| 140 |
|
| 141 |
header('Expires: ' . $expiryDate); |
| 142 |
header('Accept-Ranges: bytes'); |
| 143 |
header('Cache-Control: Public'); |
| 144 |
header('Vary: Accept-Encoding'); |
| 145 |
header('Etag: ' . $etag); |
| 146 |
|
| 147 |
$gzip = true; |
| 148 |
|
| 149 |
if (!is_null($input->server->getString('HTTP_USER_AGENT'))) { |
| 150 |
/* Facebook User Agent |
| 151 |
* facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php) |
| 152 |
* LinkedIn User Agent |
| 153 |
* LinkedInBot/1.0 (compatible; Mozilla/5.0; Jakarta Commons-HttpClient/3.1 +http://www.linkedin.com) |
| 154 |
*/ |
| 155 |
$pattern = strtolower('/facebookexternalhit|LinkedInBot/x'); |
| 156 |
|
| 157 |
if (preg_match($pattern, strtolower($input->server->getString('HTTP_USER_AGENT')))) { |
| 158 |
$gzip = false; |
| 159 |
} |
| 160 |
} |
| 161 |
|
| 162 |
if (isset($vars['gz']) && $vars['gz'] == 'gz' && $gzip) { |
| 163 |
$supported = [ |
| 164 |
'x-gzip' => 'gz', |
| 165 |
'gzip' => 'gz', |
| 166 |
'deflate' => 'deflate' |
| 167 |
]; |
| 168 |
|
| 169 |
if (!is_null($input->server->getString('HTTP_ACCEPT_ENCODING'))) { |
| 170 |
$aAccepted = array_map('trim', (array)explode(',', $input->server->getString('HTTP_ACCEPT_ENCODING'))); |
| 171 |
$encodings = array_intersect($aAccepted, array_keys($supported)); |
| 172 |
} else { |
| 173 |
$encodings = ['gzip']; |
| 174 |
} |
| 175 |
|
| 176 |
if (!empty($encodings)) { |
| 177 |
foreach ($encodings as $encoding) { |
| 178 |
if (($supported[$encoding] == 'gz') || ($supported[$encoding] == 'deflate')) { |
| 179 |
$compressedFile = gzencode( |
| 180 |
$file, |
| 181 |
4, |
| 182 |
($supported[$encoding] == 'gz') ? FORCE_GZIP : FORCE_DEFLATE |
| 183 |
); |
| 184 |
|
| 185 |
if ($compressedFile === false) { |
| 186 |
continue; |
| 187 |
} |
| 188 |
|
| 189 |
header('Content-Encoding: ' . $encoding); |
| 190 |
|
| 191 |
$file = $compressedFile; |
| 192 |
|
| 193 |
break; |
| 194 |
} |
| 195 |
} |
| 196 |
} |
| 197 |
} |
| 198 |
|
| 199 |
echo $file; |
| 200 |
} |
| 201 |
|
| 202 |
/** |
| 203 |
* @param int $fileMTime |
| 204 |
* @param string $period |
| 205 |
* |
| 206 |
* @return array |
| 207 |
*/ |
| 208 |
public static function RFC1123DateAdd(int $fileMTime, string $period): array |
| 209 |
{ |
| 210 |
$times = []; |
| 211 |
|
| 212 |
$date = new DateTime(); |
| 213 |
$date->setTimestamp($fileMTime); |
| 214 |
|
| 215 |
$times['filemtime'] = $date->format('D, d M Y H:i:s'); |
| 216 |
|
| 217 |
$date->add(DateInterval::createFromDateString($period)); |
| 218 |
$times['expiry'] = $date->format('D, d M Y H:i:s'); |
| 219 |
|
| 220 |
return $times; |
| 221 |
} |
| 222 |
} |
| 223 |
|