1
|
|
|
<?php |
|
|
|
|
2
|
|
|
/** |
3
|
|
|
* @title Gzip Class |
4
|
|
|
* @desc Compression and optimization of static files. |
5
|
|
|
* |
6
|
|
|
* @author Pierre-Henry Soria <[email protected]> |
7
|
|
|
* @copyright (c) 2012-2017, Pierre-Henry Soria. All Rights Reserved. |
8
|
|
|
* @license GNU General Public License; See PH7.LICENSE.txt and PH7.COPYRIGHT.txt in the root directory. |
9
|
|
|
* @package PH7 / Framework / Layout / Gzip |
10
|
|
|
* @version 1.7 |
11
|
|
|
*/ |
12
|
|
|
|
13
|
|
|
namespace PH7\Framework\Layout\Gzip; |
14
|
|
|
|
15
|
|
|
defined('PH7') or exit('Restricted access'); |
16
|
|
|
|
17
|
|
|
use PH7\Framework\File\File; |
18
|
|
|
use PH7\Framework\Config\Config; |
19
|
|
|
use PH7\Framework\Compress\Compress; |
20
|
|
|
use PH7\Framework\Layout\Optimization; |
21
|
|
|
use PH7\Framework\Navigation\Browser; |
22
|
|
|
use PH7\Framework\Http\Http; |
23
|
|
|
use PH7\Framework\Mvc\Request\Http as HttpRequest; |
24
|
|
|
use PH7\Framework\Error\CException\PH7InvalidArgumentException; |
25
|
|
|
|
26
|
|
|
class Gzip |
27
|
|
|
{ |
28
|
|
|
const REGEX_IMAGE_FORMAT = '/url\([\'"]*(.+?\.)(gif|png|jpg|jpeg|otf|eot|ttf|woff|svg)[\'"]*\)*/msi'; |
29
|
|
|
const CACHE_DIR = 'pH7_static/'; |
30
|
|
|
const MAX_IMG_SIZE_BASE64_CONVERTOR = 24000; // 24KB |
31
|
|
|
|
32
|
|
|
/** @var File */ |
33
|
|
|
private $oFile; |
34
|
|
|
|
35
|
|
|
/** @var HttpRequest */ |
36
|
|
|
private $oHttpRequest; |
37
|
|
|
|
38
|
|
|
/** @var string */ |
39
|
|
|
private $sBase; |
40
|
|
|
|
41
|
|
|
/** @var string */ |
42
|
|
|
private $sBaseUrl; |
43
|
|
|
|
44
|
|
|
/** @var string */ |
45
|
|
|
private $sType; |
46
|
|
|
|
47
|
|
|
/** @var string */ |
48
|
|
|
private $sDir; |
49
|
|
|
|
50
|
|
|
/** @var string */ |
51
|
|
|
private $sFiles; |
52
|
|
|
|
53
|
|
|
/** @var string */ |
54
|
|
|
private $sContents; |
55
|
|
|
|
56
|
|
|
/** @var string */ |
57
|
|
|
private $sCacheDir; |
58
|
|
|
|
59
|
|
|
/** @var array */ |
60
|
|
|
private $aElements; |
61
|
|
|
|
62
|
|
|
/** @var integer */ |
63
|
|
|
private $iIfModified; |
64
|
|
|
|
65
|
|
|
/** @var boolean */ |
66
|
|
|
private $bCaching; |
67
|
|
|
|
68
|
|
|
/** @var boolean */ |
69
|
|
|
private $bCompressor; |
70
|
|
|
|
71
|
|
|
/** @var boolean */ |
72
|
|
|
private $bDataUri; |
73
|
|
|
|
74
|
|
|
/** @var boolean */ |
75
|
|
|
private $bGzipContent; |
76
|
|
|
|
77
|
|
|
/** @var boolean */ |
78
|
|
|
private $bIsGzip; |
79
|
|
|
|
80
|
|
|
/** @var string|boolean */ |
81
|
|
|
private $mEncoding; |
82
|
|
|
|
83
|
|
|
public function __construct() |
84
|
|
|
{ |
85
|
|
|
$this->oFile = new File; |
86
|
|
|
$this->oHttpRequest = new HttpRequest; |
87
|
|
|
|
88
|
|
|
$this->bCaching = (bool) Config::getInstance()->values['cache']['enable.static.cache']; |
89
|
|
|
$this->bCompressor = (bool) Config::getInstance()->values['cache']['enable.static.minify']; |
90
|
|
|
$this->bGzipContent = (bool) Config::getInstance()->values['cache']['enable.static.gzip_compress']; |
91
|
|
|
$this->bDataUri = (bool) Config::getInstance()->values['cache']['enable.static.data_uri']; |
92
|
|
|
|
93
|
|
|
$this->bIsGzip = $this->isGzip(); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Set cache directory. |
98
|
|
|
* If the directory is not correct, the method will cause an exception. |
99
|
|
|
* If you do not use this method, a default directory will be created. |
100
|
|
|
* |
101
|
|
|
* @param string $sCacheDir |
102
|
|
|
* |
103
|
|
|
* @return void |
104
|
|
|
* |
105
|
|
|
* @throws PH7InvalidArgumentException If the cache directory does not exist. |
106
|
|
|
*/ |
107
|
|
|
public function setCacheDir($sCacheDir) |
108
|
|
|
{ |
109
|
|
|
if (is_dir($sCacheDir)) |
110
|
|
|
$this->sCacheDir = $sCacheDir; |
111
|
|
|
else |
112
|
|
|
throw new PH7InvalidArgumentException('"' . $sCacheDir . '" cache directory cannot be found!'); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Displays compressed files. |
117
|
|
|
* |
118
|
|
|
* @return void |
119
|
|
|
* |
120
|
|
|
* @throws Exception If the cache file couldn't be written. |
121
|
|
|
* |
122
|
|
|
* @throws \PH7\Framework\File\Exception |
123
|
|
|
*/ |
124
|
|
|
public function run() |
125
|
|
|
{ |
126
|
|
|
// Determine the directory and type we should use |
127
|
|
|
if (!$this->oHttpRequest->getExists('t') || ($this->oHttpRequest->get('t') !== |
128
|
|
|
'html' && $this->oHttpRequest->get('t') !== 'css' && $this->oHttpRequest->get('t') !== 'js')) |
129
|
|
|
{ |
130
|
|
|
Http::setHeadersByCode(503); |
131
|
|
|
exit('Invalid type file!'); |
|
|
|
|
132
|
|
|
} |
133
|
|
|
$this->sType = ($this->oHttpRequest->get('t') === 'js') ? 'javascript' : $this->oHttpRequest->get('t'); |
134
|
|
|
|
135
|
|
|
// Directory |
136
|
|
|
if (!$this->oHttpRequest->getExists('d')) |
137
|
|
|
{ |
138
|
|
|
Http::setHeadersByCode(503); |
139
|
|
|
exit('No directory specified!'); |
|
|
|
|
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
$this->sDir = $this->oHttpRequest->get('d'); |
143
|
|
|
$this->sBase = $this->oFile->checkExtDir(realpath($this->sDir)); |
144
|
|
|
$this->sBaseUrl = $this->clearUrl($this->oFile->checkExtDir($this->sDir)); |
145
|
|
|
|
146
|
|
|
// The Files |
147
|
|
|
if (!$this->oHttpRequest->getExists('f')) |
148
|
|
|
{ |
149
|
|
|
Http::setHeadersByCode(503); |
150
|
|
|
exit('No file specified!'); |
|
|
|
|
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$this->sFiles = $this->oHttpRequest->get('f'); |
154
|
|
|
$this->aElements = explode(',', $this->sFiles); |
155
|
|
|
|
156
|
|
|
foreach ($this->aElements as $sElement) |
157
|
|
|
{ |
158
|
|
|
$sPath = realpath($this->sBase . $sElement); |
159
|
|
|
|
160
|
|
|
if (($this->sType == 'html' && substr($sPath, -5) != '.html') || ($this-> |
|
|
|
|
161
|
|
|
_sType == 'javascript' && substr($sPath, -3) != '.js') || ($this->sType == 'css' && substr($sPath, -4) != '.css')) |
162
|
|
|
{ |
163
|
|
|
Http::setHeadersByCode(403); |
164
|
|
|
exit('Error file extension.'); |
|
|
|
|
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
if (substr($sPath, 0, strlen($this->sBase)) != $this->sBase || !is_file($sPath)) { |
168
|
|
|
Http::setHeadersByCode(404); |
169
|
|
|
exit('The file not found!'); |
|
|
|
|
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$this->setHeaders(); |
174
|
|
|
|
175
|
|
|
// If the cache is enabled, reads cache and displays, otherwise reads and displays the contents. |
176
|
|
|
$this->bCaching ? $this->cache() : $this->getContents(); |
177
|
|
|
|
178
|
|
|
echo $this->sContents; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Set Caching. |
183
|
|
|
* |
184
|
|
|
* @return string The cached contents. |
185
|
|
|
* |
186
|
|
|
* @throws Exception If the cache file couldn't be written. |
187
|
|
|
* |
188
|
|
|
* @throws \PH7\Framework\File\Exception If the file cannot be created. |
189
|
|
|
*/ |
190
|
|
|
public function cache() |
|
|
|
|
191
|
|
|
{ |
192
|
|
|
$this->checkCacheDir(); |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Try the cache first to see if the combined files were already generated. |
196
|
|
|
*/ |
197
|
|
|
|
198
|
|
|
$oBrowser = new Browser; |
199
|
|
|
|
200
|
|
|
$this->iIfModified = (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE'])) ? substr($_SERVER['HTTP_IF_MODIFIED_SINCE'], 0, 29) : null; |
|
|
|
|
201
|
|
|
|
202
|
|
|
$this->sCacheDir .= $this->oHttpRequest->get('t') . PH7_DS; |
203
|
|
|
$this->oFile->createDir($this->sCacheDir); |
204
|
|
|
$sExt = ($this->bIsGzip) ? 'gz' : 'cache'; |
205
|
|
|
$sCacheFile = md5($this->sType . $this->sDir . $this->sFiles) . PH7_DOT . $sExt; |
206
|
|
|
|
207
|
|
|
foreach ($this->aElements as $sElement) |
208
|
|
|
{ |
209
|
|
|
$sPath = realpath($this->sBase . $sElement); |
210
|
|
|
|
211
|
|
|
if ($this->oFile->getModifTime($sPath) > $this->oFile->getModifTime($this->sCacheDir . $sCacheFile)) |
212
|
|
|
{ |
213
|
|
|
if (!empty($this->iIfModified) && $this->oFile->getModifTime($sPath) > $this->oFile->getModifTime($this->iIfModified)) |
214
|
|
|
$oBrowser->noCache(); |
215
|
|
|
|
216
|
|
|
// Get contents of the files |
217
|
|
|
$this->getContents(); |
218
|
|
|
|
219
|
|
|
// Store the file in the cache |
220
|
|
|
if (!$this->oFile->putFile($this->sCacheDir . $sCacheFile, $this->sContents)) |
221
|
|
|
throw new Exception('Couldn\'t write cache file: \'' . $this->sCacheDir . $sCacheFile . '\''); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
if ($this->oHttpRequest->getMethod() != 'HEAD') |
226
|
|
|
{ |
227
|
|
|
$oBrowser->cache(); |
228
|
|
|
//header('Not Modified', true, 304); // Warning: It can causes problems (ERR_FILE_NOT_FOUND) |
|
|
|
|
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
unset($oBrowser); |
232
|
|
|
|
233
|
|
|
if (!$this->sContents = $this->oFile->getFile($this->sCacheDir . $sCacheFile)) |
|
|
|
|
234
|
|
|
throw new Exception('Couldn\'t read cache file: \'' . $this->sCacheDir . $sCacheFile . '\''); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* Routing for files compressing. |
239
|
|
|
* |
240
|
|
|
* @return void |
241
|
|
|
*/ |
242
|
|
|
protected function makeCompress() |
243
|
|
|
{ |
244
|
|
|
$oCompress = new Compress; |
245
|
|
|
|
246
|
|
|
switch ($this->sType) |
247
|
|
|
{ |
248
|
|
|
case 'html': |
249
|
|
|
$this->sContents = $oCompress->parseHtml($this->sContents); |
250
|
|
|
break; |
251
|
|
|
|
252
|
|
|
case 'css': |
253
|
|
|
$this->sContents = $oCompress->parseCss($this->sContents); |
254
|
|
|
break; |
255
|
|
|
|
256
|
|
|
case 'javascript': |
257
|
|
|
$this->sContents = $oCompress->parseJs($this->sContents); |
258
|
|
|
break; |
259
|
|
|
|
260
|
|
|
default: |
261
|
|
|
Http::setHeadersByCode(503); |
262
|
|
|
exit('Invalid type file!'); |
|
|
|
|
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
unset($oCompress); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
/** |
269
|
|
|
* Transform the contents into a gzip compressed string. |
270
|
|
|
* |
271
|
|
|
* @return void |
272
|
|
|
*/ |
273
|
|
|
protected function gzipContent() |
274
|
|
|
{ |
275
|
|
|
$this->sContents = gzencode($this->sContents, 9, FORCE_GZIP); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Get contents of the files. |
280
|
|
|
* |
281
|
|
|
* @return void |
282
|
|
|
*/ |
283
|
|
|
protected function getContents() |
284
|
|
|
{ |
285
|
|
|
$this->sContents = ''; |
286
|
|
|
foreach ($this->aElements as $sElement) { |
287
|
|
|
$this->sContents .= File::EOL . $this->oFile->getUrlContents(PH7_URL_ROOT . $this->sBaseUrl . $sElement); |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
if ($this->sType == 'css') |
291
|
|
|
{ |
292
|
|
|
$this->parseVariable(); |
293
|
|
|
$this->getSubCssFile(); |
294
|
|
|
$this->getImageIntoCss(); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
if ($this->sType == 'javascript') |
298
|
|
|
{ |
299
|
|
|
$this->parseVariable(); |
300
|
|
|
$this->getSubJsFile(); |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
if ($this->bCompressor) $this->makeCompress(); |
304
|
|
|
|
305
|
|
|
if ($this->bCaching) $this->sContents = '/*Cached on ' . gmdate('d M Y H:i:s') . '*/' . File::EOL . $this->sContents; |
306
|
|
|
|
307
|
|
|
if ($this->bIsGzip) $this->gzipContent(); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* @return void |
312
|
|
|
*/ |
313
|
|
|
protected function setHeaders() |
314
|
|
|
{ |
315
|
|
|
// Send Content-Type |
316
|
|
|
header('Content-Type: text/' . $this->sType); |
317
|
|
|
header('Vary: Accept-Encoding'); |
318
|
|
|
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600*24*10) . ' GMT'); // 10 days |
319
|
|
|
|
320
|
|
|
// Send compressed contents |
321
|
|
|
if ($this->bIsGzip) header('Content-Encoding: ' . $this->mEncoding); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Check if gzip is activate. |
326
|
|
|
* |
327
|
|
|
* @return boolean Returns FALSE if compression is disabled or is not valid, otherwise returns TRUE |
328
|
|
|
*/ |
329
|
|
|
protected function isGzip() |
330
|
|
|
{ |
331
|
|
|
$this->mEncoding = (new Browser)->encoding(); |
332
|
|
|
return (!$this->bGzipContent ? false : ($this->mEncoding !== false ? true : false)); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Parser the CSS/JS variables in cascading style sheets and JavaScript files. |
337
|
|
|
* |
338
|
|
|
* @return void |
339
|
|
|
*/ |
340
|
|
|
protected function parseVariable() |
341
|
|
|
{ |
342
|
|
|
$sBaseUrl = $this->sBaseUrl; |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* $getCurrentTplName is used in "variables.inc.php" file |
346
|
|
|
*/ |
347
|
|
|
$getCurrentTplName = function () use ($sBaseUrl) { |
348
|
|
|
$aDirs = explode('/', $sBaseUrl); |
349
|
|
|
return !empty($aDirs[2]) ? $aDirs[2] : PH7_DEFAULT_THEME; |
350
|
|
|
}; |
351
|
|
|
|
352
|
|
|
$this->setVariables( include('variables.inc.php') ); |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @return void |
357
|
|
|
*/ |
358
|
|
|
protected function getSubCssFile() |
359
|
|
|
{ |
360
|
|
|
// We also collect the files included in the CSS files. So we can also cache and compressed. |
361
|
|
|
preg_match_all('/@import\s+url\([\'"]*(.+?\.)(css)[\'"]*\)\s{0,};/msi', $this->sContents, $aHit, PREG_PATTERN_ORDER); |
362
|
|
|
|
363
|
|
|
for ($i = 0, $iCountHit = count($aHit[0]); $i < $iCountHit; $i++) { |
364
|
|
|
$this->sContents = str_replace($aHit[0][$i], '', $this->sContents); |
365
|
|
|
$this->sContents .= File::EOL . $this->oFile->getUrlContents($aHit[1][$i] . $aHit[2][$i]); |
366
|
|
|
} |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @return void |
371
|
|
|
*/ |
372
|
|
|
protected function getSubJsFile() |
373
|
|
|
{ |
374
|
|
|
// We also collect the files included in the JavaScript files. So we can also cache and compressed. |
375
|
|
|
preg_match_all('/include\([\'"]*(.+?\.)(js)[\'"]*\)\s{0,};/msi', $this->sContents, $aHit, PREG_PATTERN_ORDER); |
376
|
|
|
|
377
|
|
|
for ($i = 0, $iCountHit = count($aHit[0]); $i < $iCountHit; $i++) |
378
|
|
|
{ |
379
|
|
|
$this->sContents = str_replace($aHit[0][$i], '', $this->sContents); |
380
|
|
|
$this->sContents .= File::EOL . $this->oFile->getUrlContents($aHit[1][$i] . $aHit[2][$i]); |
381
|
|
|
} |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* Get the images into the CSS files. |
386
|
|
|
* |
387
|
|
|
* @return void |
388
|
|
|
*/ |
389
|
|
|
private function getImageIntoCss() |
390
|
|
|
{ |
391
|
|
|
preg_match_all(self::REGEX_IMAGE_FORMAT, $this->sContents, $aHit, PREG_PATTERN_ORDER); |
392
|
|
|
|
393
|
|
|
for ($i = 0, $iCountHit = count($aHit[0]); $i < $iCountHit; $i++) { |
394
|
|
|
$sImgPath = PH7_PATH_ROOT . $this->sBaseUrl . $aHit[1][$i] . $aHit[2][$i]; |
395
|
|
|
$sImgUrl = PH7_URL_ROOT . $this->sBaseUrl . $aHit[1][$i] . $aHit[2][$i]; |
396
|
|
|
|
397
|
|
|
// If the image-file exists and if file-size is lower than 24 KB, we convert it into base64 data URI |
398
|
|
|
if ($this->bDataUri && is_file($sImgPath) && $this->oFile->size($sImgPath) < self::MAX_IMG_SIZE_BASE64_CONVERTOR) { |
399
|
|
|
$this->sContents = str_replace($aHit[0][$i], 'url(' . Optimization::dataUri($sImgPath, $this->oFile) . ')', $this->sContents); |
400
|
|
|
} else { |
401
|
|
|
$this->sContents = str_replace($aHit[0][$i], 'url(' . $sImgUrl . ')', $this->sContents); |
402
|
|
|
} |
403
|
|
|
} |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* Set CSS/JS variables. |
408
|
|
|
* |
409
|
|
|
* @param array $aVars Variable names containing the values. |
410
|
|
|
* |
411
|
|
|
* @return void |
412
|
|
|
*/ |
413
|
|
|
private function setVariables(array $aVars) |
414
|
|
|
{ |
415
|
|
|
// Replace the variable name by the content |
416
|
|
|
foreach ($aVars as $sKey => $sVal) |
417
|
|
|
$this->sContents = str_replace('[$' . $sKey . ']', $sVal, $this->sContents); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
/** |
421
|
|
|
* Checks if the cache directory has been defined otherwise we create a default directory. |
422
|
|
|
* If the directory cache does not exist, it creates a directory. |
423
|
|
|
* |
424
|
|
|
* @return void |
425
|
|
|
*/ |
426
|
|
|
private function checkCacheDir() |
427
|
|
|
{ |
428
|
|
|
$this->sCacheDir = (empty($this->sCacheDir)) ? PH7_PATH_CACHE . static::CACHE_DIR : $this->sCacheDir; |
429
|
|
|
} |
430
|
|
|
|
431
|
|
|
/** |
432
|
|
|
* Remove backslashes on Windows. |
433
|
|
|
* |
434
|
|
|
* @param string $sPath |
435
|
|
|
* |
436
|
|
|
* @return string The path without backslashes and/or double slashes. |
437
|
|
|
*/ |
438
|
|
|
private function clearUrl($sPath) |
439
|
|
|
{ |
440
|
|
|
return str_replace(array('\\', '//'), '/', $sPath); |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.