Completed
Branch master (35850d)
by Pierre-Henry
35:39
created

Gzip::clearUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 26 and the first side effect is on line 15.

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.

Loading history...
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!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method run() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method run() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method run() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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->
0 ignored issues
show
Bug introduced by
The property _sType does not seem to exist. Did you mean sType?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
161
                _sType == 'javascript' && substr($sPath, -3) != '.js') || ($this->sType == 'css' && substr($sPath, -4) != '.css'))
162
            {
163
                Http::setHeadersByCode(403);
164
                exit('Error file extension.');
0 ignored issues
show
Coding Style Compatibility introduced by
The method run() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method run() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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()
0 ignored issues
show
Coding Style introduced by
cache uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like !empty($_SERVER['HTTP_IF..._SINCE'], 0, 29) : null can also be of type string. However, the property $iIfModified is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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)
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
229
        }
230
231
        unset($oBrowser);
232
233
        if (!$this->sContents = $this->oFile->getFile($this->sCacheDir . $sCacheFile))
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->oFile->getFile($t...CacheDir . $sCacheFile) can also be of type boolean. However, the property $sContents is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
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!');
0 ignored issues
show
Coding Style Compatibility introduced by
The method makeCompress() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
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