Completed
Pull Request — master (#31)
by
unknown
02:13
created

View::_getSummaryFilesHash()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 6
Bugs 2 Features 1
Metric Value
c 6
b 2
f 1
dl 0
loc 13
ccs 0
cts 0
cp 0
rs 9.2
cc 4
eloc 7
nc 3
nop 1
crap 20
1
<?php
2
/**
3
 * View.php
4
 * @author Revin Roman
5
 * @link https://rmrevin.ru
6
 */
7
8
namespace rmrevin\yii\minify;
9
10
use yii\helpers;
11
12
/**
13
 * Class View
14
 * @package rmrevin\yii\minify
15
 */
16
class View extends \yii\web\View
17
{
18
19
    /** @var bool */
20
    public $enableMinify = true;
21
22
    /** @var bool */
23
    public $minifyCss = true;
24
25
    /** @var bool */
26
    public $minifyJs = true;
27
28
    /** @var bool */
29
    public $removeComments = true;
30
31
    /** @var string path alias to web base (in url) */
32
    public $web_path = '@web';
33
34
    /** @var string path alias to web base (absolute) */
35
    public $base_path = '@webroot';
36
37
    /** @var string path alias to save minify result */
38
    public $minify_path = '@webroot/minify';
39
40
    /** @var array positions of js files to be minified */
41
    public $js_position = [self::POS_END, self::POS_HEAD];
42
43
    /** @var bool|string charset forcibly assign, otherwise will use all of the files found charset */
44
    public $force_charset = false;
45
46
    /** @var bool whether to change @import on content */
47
    public $expand_imports = true;
48
49
    /** @var int */
50
    public $css_linebreak_pos = 2048;
51
52
    /** @var int|bool chmod of minified file. If false chmod not set */
53
    public $file_mode = 0664;
54
55
    /** @var array schemes that will be ignored during normalization url */
56
    public $schemas = ['//', 'http://', 'https://', 'ftp://'];
57
58
    /** @var bool do I need to compress the result html page. */
59
    public $compress_output = false;
60
61
    /**
62
     * @var array options for compressing output result
63
     *   * extra - use more compact algorithm
64
     *   * no-comments - cut all the html comments
65
     */
66
    public $compress_options = ['extra' => true];
67
68
    /**
69
     * @throws \rmrevin\yii\minify\Exception
70
     */
71 6
    public function init()
72
    {
73
        parent::init();
74
75
        $minify_path = $this->minify_path = (string)\Yii::getAlias($this->minify_path);
76
        if (!file_exists($minify_path)) {
77
            helpers\FileHelper::createDirectory($minify_path);
78
        }
79
80
        if (!is_readable($minify_path)) {
81
            throw new Exception('Directory for compressed assets is not readable.');
82
        }
83
84
        if (!is_writable($minify_path)) {
85
            throw new Exception('Directory for compressed assets is not writable.');
86
        }
87
88 6
        if (true === $this->compress_output) {
89
            \Yii::$app->response->on(\yii\web\Response::EVENT_BEFORE_SEND, function (\yii\base\Event $Event) {
90
                /** @var \yii\web\Response $Response */
91
                $Response = $Event->sender;
92
                if ($Response->format === \yii\web\Response::FORMAT_HTML) {
93
                    if (!empty($Response->data)) {
94
                        $Response->data = HtmlCompressor::compress($Response->data, $this->compress_options);
95
                    }
96
97
                    if (!empty($Response->content)) {
98
                        $Response->content = HtmlCompressor::compress($Response->content, $this->compress_options);
0 ignored issues
show
Documentation Bug introduced by
It seems like \rmrevin\yii\minify\Html...this->compress_options) can also be of type false. However, the property $content 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...
99
                    }
100
                }
101
            });
102
        }
103 6
    }
104
105
    /**
106
     * @inheritdoc
107
     */
108 4
    public function endPage($ajaxMode = false)
109
    {
110
        $this->trigger(self::EVENT_END_PAGE);
111
112
        $content = ob_get_clean();
113 4
        foreach (array_keys($this->assetBundles) as $bundle) {
114
            $this->registerAssetFiles($bundle);
115
        }
116
117 4
        if (true === $this->enableMinify && !$ajaxMode) {
118 4
            if (true === $this->minifyCss) {
119
                $this->minifyCSS();
120
            }
121
122 4
            if (true === $this->minifyJs) {
123
                $this->minifyJS();
124
            }
125
        }
126
127
        echo strtr(
128
            $content,
129
            [
130
                self::PH_HEAD => $this->renderHeadHtml(),
131
                self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
132
                self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
133 4
            ]
134
        );
135
136
        $this->clear();
137 4
    }
138
139
    /**
140
     * @return self
141
     */
142 4
    protected function minifyCSS()
143
    {
144 4
        if (!empty($this->cssFiles)) {
145 4
            $cssFiles = $this->cssFiles;
146
147 4
            $this->cssFiles = [];
148
149 4
            $toMinify = [];
150
151 View Code Duplication
            foreach ($cssFiles as $file => $html) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
152
                if ($this->thisFileNeedMinify($file, $html)) {
153
                    $toMinify[$file] = $html;
154
                } else {
155
                    if (!empty($toMinify)) {
156
                        $this->processMinifyCss($toMinify);
157
158
                        $toMinify = [];
159
                    }
160
161
                    $this->cssFiles[$file] = $html;
162
                }
163
            }
164
165 4
            if (!empty($toMinify)) {
166
                $this->processMinifyCss($toMinify);
167
            }
168
169 4
            unset($toMinify);
170
        }
171
172 4
        return $this;
173
    }
174
175
    /**
176
     * @param array $files
177
     */
178 4
    protected function processMinifyCss($files)
179
    {
180
        $resultFile = $this->minify_path . '/' . $this->_getSummaryFilesHash($files) . '.css';
181
182
        if (!file_exists($resultFile)) {
183 4
            $css = '';
184
185
            foreach ($files as $file => $html) {
186
                $path = dirname($file);
187
                $file = $this->getAbsoluteFilePath($file);
188
189
                $content = file_get_contents($file);
190
191
                preg_match_all('|url\(([^)]+)\)|is', $content, $m);
192
                if (!empty($m[0])) {
193
                    $result = [];
194
195
                    foreach ($m[0] as $k => $v) {
196
                        if (in_array(strpos($m[1][$k], 'data:'), [0, 1], true)) {
197 4
                            continue;
198
                        }
199
200
                        $url = str_replace(['\'', '"'], '', $m[1][$k]);
201
202
                        if ($this->isUrl($url)) {
203
                            $result[$m[1][$k]] = '\'' . $url . '\'';
204
                        } else {
205 4
                            $result[$m[1][$k]] = '\'' . $path . '/' . $url . '\'';
206
                        }
207
                    }
208
209
                    $content = str_replace(array_keys($result), array_values($result), $content);
210
                }
211
212
                $css .= $content;
213
            }
214
215
            $this->expandImports($css);
216
217
            $this->removeCssComments($css);
218
219
            $css = (new \CSSmin())
220
                ->run($css, $this->css_linebreak_pos);
221
222 4
            if (false !== $this->force_charset) {
223 2
                $charsets = '@charset "' . (string)$this->force_charset . '";' . "\n";
224
            } else {
225
                $charsets = $this->collectCharsets($css);
226 2
            }
227
228
            $imports = $this->collectImports($css);
229
            $fonts = $this->collectFonts($css);
230
231
            file_put_contents($resultFile, $charsets . $imports . $fonts . $css);
232
233 4
            if (false !== $this->file_mode) {
234
                @chmod($resultFile, $this->file_mode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
235
            }
236
        }
237
238
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
239
240
        $this->cssFiles[$file] = helpers\Html::cssFile($file);
241 4
    }
242
243
    /**
244
     * @return self
245
     */
246 4
    protected function minifyJS()
247
    {
248 4
        if (!empty($this->jsFiles)) {
249 4
            $jsFiles = $this->jsFiles;
250
251
            foreach ($jsFiles as $position => $files) {
252
                if (false === in_array($position, $this->js_position, true)) {
253 4
                    $this->jsFiles[$position] = [];
254
                    foreach ($files as $file => $html) {
255 4
                        $this->jsFiles[$position][$file] = $html;
256
                    }
257
                } else {
258 4
                    $this->jsFiles[$position] = [];
259
260 4
                    $toMinify = [];
261
262 View Code Duplication
                    foreach ($files as $file => $html) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
263
                        if ($this->thisFileNeedMinify($file, $html)) {
264 4
                            $toMinify[$file] = $html;
265
                        } else {
266 4
                            if (!empty($toMinify)) {
267
                                $this->processMinifyJs($position, $toMinify);
268
269
                                $toMinify = [];
270
                            }
271
272 4
                            $this->jsFiles[$position][$file] = $html;
273 4
                        }
274
                    }
275
276 4
                    if (!empty($toMinify)) {
277
                        $this->processMinifyJs($position, $toMinify);
278
                    }
279
280 4
                    unset($toMinify);
281 4
                }
282
            }
283
        }
284
285 4
        return $this;
286
    }
287
288
    /**
289
     * @param integer $position
290
     * @param array $files
291
     */
292 4
    protected function processMinifyJs($position, $files)
293
    {
294
        $resultFile = sprintf('%s/%s.js', $this->minify_path, $this->_getSummaryFilesHash($files));
295
        if (!file_exists($resultFile)) {
296 4
            $js = '';
297
            foreach ($files as $file => $html) {
298
                $file = $this->getAbsoluteFilePath($file);
299
                $js .= file_get_contents($file) . ';' . PHP_EOL;
300
            }
301
302
            $this->removeJsComments($js);
0 ignored issues
show
Unused Code introduced by
The call to the method rmrevin\yii\minify\View::removeJsComments() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
303
304
            $compressedJs = (new \JSMin($js))
305
                ->min();
306
307
            file_put_contents($resultFile, $compressedJs);
308
309 4
            if (false !== $this->file_mode) {
310
                @chmod($resultFile, $this->file_mode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
311
            }
312
        }
313
314
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
315
316
        $this->jsFiles[$position][$file] = helpers\Html::jsFile($file);
317 4
    }
318
319
    /**
320
     * @param string $url
321
     * @param boolean $checkSlash
322
     * @return bool
323
     */
324
    protected function isUrl($url, $checkSlash = true)
325
    {
326
        $regexp = '#^(' . implode('|', $this->schemas) . ')#is';
327
        if ($checkSlash) {
328
            $regexp = '#^(/|\\\\|' . implode('|', $this->schemas) . ')#is';
329
        }
330
331
        return (bool)preg_match($regexp, $url);
332
    }
333
334
    /**
335
     * @param string $string
336
     * @return bool
337
     */
338
    protected function isContainsConditionalComment($string)
339
    {
340
        return strpos($string, '<![endif]-->') !== false;
341
    }
342
343
    /**
344
     * @param string $file
345
     * @param string $html
346
     * @return bool
347
     */
348
    protected function thisFileNeedMinify($file, $html)
349
    {
350
        return !$this->isUrl($file, false) && !$this->isContainsConditionalComment($html);
351
    }
352
353
    /**
354
     * @param string $code
355
     * @return string
356
     */
357 2
    protected function collectCharsets(&$code)
358
    {
359
        return $this->_collect($code, '|\@charset[^;]+|is', function ($string) {
360 2
            return $string . ';';
361
        });
362
    }
363
364
    /**
365
     * @param string $code
366
     * @return string
367
     */
368
    protected function collectImports(&$code)
369
    {
370
        return $this->_collect($code, '|\@import[^;]+|is', function ($string) {
371
            return $string . ';';
372
        });
373
    }
374
375
    /**
376
     * @param string $code
377
     * @return string
378
     */
379
    protected function collectFonts(&$code)
380
    {
381
        return $this->_collect($code, '|\@font-face\{[^}]+\}|is', function ($string) {
382
            return $string;
383
        });
384
    }
385
386
    /**
387
     * @param string $code
388
     */
389 4
    protected function removeCssComments(&$code)
390
    {
391 4
        if (true === $this->removeComments) {
392
            $code = preg_replace('#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $code);
393
        }
394 4
    }
395
396
    /**
397
     * @param string $code
398
     */
399 4
    protected function removeJsComments(&$code)
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
400
    {
401
        // @todo
402 4
        if (true === $this->removeComments) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
403
            //$code = preg_replace('', '', $code);
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
404
        }
405 4
    }
406
407
    /**
408
     * @param string $code
409
     */
410 4
    protected function expandImports(&$code)
411
    {
412 4
        if (true === $this->expand_imports) {
413
            preg_match_all('|\@import\s([^;]+);|is', str_replace('&amp;', '&', $code), $m);
414 2
            if (!empty($m[0])) {
415 2
                foreach ($m[0] as $k => $v) {
416
                    $import_url = $m[1][$k];
417
                    if (!empty($import_url)) {
418
                        $import_content = $this->_getImportContent($import_url);
419
                        if (!empty($import_content)) {
420
                            $code = str_replace($m[0][$k], $import_content, $code);
421
                        }
422
                    }
423
                }
424
            }
425
        }
426 4
    }
427
428
    /**
429
     * @param string $url
430
     * @return null|string
431
     */
432
    protected function _getImportContent($url)
433
    {
434
        $result = null;
435
436
        if ('url(' === helpers\StringHelper::byteSubstr($url, 0, 4)) {
437
            $url = str_replace(['url(\'', 'url("', 'url(', '\')', '")', ')'], '', $url);
438
439
            if (helpers\StringHelper::byteSubstr($url, 0, 2) === '//') {
440
                $url = preg_replace('|^//|', 'http://', $url, 1);
441
            }
442
443
            if (!empty($url)) {
444
                $result = file_get_contents($url);
445
            }
446
        }
447
448
        return $result;
449
    }
450
451
    /**
452
     * @param string $code
453
     * @param string $pattern
454
     * @param callable $handler
455
     * @return string
456
     */
457
    protected function _collect(&$code, $pattern, $handler)
458
    {
459
        $result = '';
460
461
        preg_match_all($pattern, $code, $m);
462
        foreach ($m[0] as $string) {
463
            $string = $handler($string);
464
            $code = str_replace($string, '', $code);
465
466
            $result .= $string . PHP_EOL;
467
        }
468
469
        return $result;
470
    }
471
472
    /**
473
     * @param string $file
474
     * @return string
475
     */
476
    protected function cleanFileName($file)
477
    {
478
        return (strpos($file, '?')) ? parse_url($file, PHP_URL_PATH) : $file;
479
    }
480
481
    /**
482
     * @param string $file
483
     * @return string
484
     */
485
    protected function getAbsoluteFilePath($file)
486
    {
487
        return \Yii::getAlias($this->base_path) . str_replace(\Yii::getAlias($this->web_path), '', $this->cleanFileName($file));
488
    }
489
490
    /**
491
     * @param array $files
492
     * @return string
493
     */
494
    protected function _getSummaryFilesHash($files)
495
    {
496
        $result = '';
497
        foreach ($files as $file => $html) {
498
            $path = $this->getAbsoluteFilePath($file);
499
500
            if ($this->thisFileNeedMinify($file, $html) && file_exists($path)) {
501
                $result .= sha1_file($path);
502
            }
503
        }
504
505
        return sha1($result);
506
    }
507
}
508