Completed
Push — master ( bdcaed...b839dd )
by Revin
12:51
created

View::removeJsComments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
ccs 3
cts 3
cp 1
rs 9.4285
cc 2
eloc 2
nc 2
nop 1
crap 2
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
     * @throws \rmrevin\yii\minify\Exception
63
     */
64 3
    public function init()
65
    {
66
        parent::init();
67
68
        $minify_path = $this->minify_path = (string)\Yii::getAlias($this->minify_path);
69
        if (!file_exists($minify_path)) {
70
            helpers\FileHelper::createDirectory($minify_path);
71
        }
72
73
        if (!is_readable($minify_path)) {
74
            throw new Exception('Directory for compressed assets is not readable.');
75
        }
76
77
        if (!is_writable($minify_path)) {
78
            throw new Exception('Directory for compressed assets is not writable.');
79
        }
80
81 3
        if (true === $this->compress_output) {
82
            \Yii::$app->response->on(\yii\web\Response::EVENT_BEFORE_SEND, function (\yii\base\Event $Event) {
83
                /** @var \yii\web\Response $Response */
84
                $Response = $Event->sender;
85
                if ($Response->format === \yii\web\Response::FORMAT_HTML) {
86 View Code Duplication
                    if (!empty($Response->data)) {
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...
87
                        $Response->data = HtmlCompressor::compress($Response->data, ['extra' => true]);
88
                    }
89
90 View Code Duplication
                    if (!empty($Response->content)) {
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...
91
                        $Response->content = HtmlCompressor::compress($Response->content, ['extra' => true]);
0 ignored issues
show
Documentation Bug introduced by
It seems like \rmrevin\yii\minify\Html...array('extra' => true)) 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...
92
                    }
93
                }
94
            });
95
        }
96 3
    }
97
98
    /**
99
     * @inheritdoc
100
     */
101 2
    public function endPage($ajaxMode = false)
102
    {
103
        $this->trigger(self::EVENT_END_PAGE);
104
105
        $content = ob_get_clean();
106 2
        foreach (array_keys($this->assetBundles) as $bundle) {
107
            $this->registerAssetFiles($bundle);
108
        }
109
110 2
        if (true === $this->enableMinify) {
111 2
            if (true === $this->minifyCss) {
112
                $this->minifyCSS();
113
            }
114
115 2
            if (true === $this->minifyJs) {
116
                $this->minifyJS();
117
            }
118
        }
119
120
        echo strtr(
121
            $content,
122
            [
123
                self::PH_HEAD => $this->renderHeadHtml(),
124
                self::PH_BODY_BEGIN => $this->renderBodyBeginHtml(),
125
                self::PH_BODY_END => $this->renderBodyEndHtml($ajaxMode),
126 2
            ]
127
        );
128
129
        $this->clear();
130 2
    }
131
132
    /**
133
     * @return self
134
     */
135 2
    protected function minifyCSS()
136
    {
137 2
        if (!empty($this->cssFiles)) {
138 2
            $cssFiles = $this->cssFiles;
139
140 2
            $this->cssFiles = [];
141
142 2
            $toMinify = [];
143
144 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...
145
                if ($this->thisFileNeedMinify($file, $html)) {
146
                    $toMinify[$file] = $html;
147
                } else {
148
                    if (!empty($toMinify)) {
149
                        $this->processMinifyCss($toMinify);
150
151
                        $toMinify = [];
152
                    }
153
154
                    $this->cssFiles[$file] = $html;
155
                }
156
            }
157
158 2
            if (!empty($toMinify)) {
159
                $this->processMinifyCss($toMinify);
160
            }
161
162 2
            unset($toMinify);
163
        }
164
165 2
        return $this;
166
    }
167
168
    /**
169
     * @param array $files
170
     */
171 2
    protected function processMinifyCss($files)
172
    {
173
        $resultFile = $this->minify_path . '/' . $this->_getSummaryFilesHash($files) . '.css';
174
175
        if (!file_exists($resultFile)) {
176 2
            $css = '';
177
178
            foreach ($files as $file => $html) {
179
                $file = str_replace(\Yii::getAlias($this->web_path), '', $file);
180
181
                $content = file_get_contents(\Yii::getAlias($this->base_path) . $file);
182
183
                preg_match_all('|url\(([^)]+)\)|is', $content, $m);
184
                if (!empty($m[0])) {
185
                    $path = dirname($file);
186
                    $result = [];
187
                    foreach ($m[0] as $k => $v) {
188
                        if (in_array(strpos($m[1][$k], 'data:'), [0, 1], true)) {
189 2
                            continue;
190
                        }
191
                        $url = str_replace(['\'', '"'], '', $m[1][$k]);
192
                        if ($this->isUrl($url)) {
193
                            $result[$m[1][$k]] = '\'' . $url . '\'';
194
                        } else {
195
                            $result[$m[1][$k]] = '\'' . \Yii::getAlias($this->web_path) . $path . '/' . $url . '\'';
196
                        }
197
                    }
198
                    $content = str_replace(array_keys($result), array_values($result), $content);
199
                }
200
201
                $css .= $content;
202
            }
203
204
            $this->expandImports($css);
205
206
            $this->removeCssComments($css);
207
208
            $css = (new \CSSmin())
209
                ->run($css, $this->css_linebreak_pos);
210
211 2
            if (false !== $this->force_charset) {
212 1
                $charsets = '@charset "' . (string)$this->force_charset . '";' . "\n";
213
            } else {
214
                $charsets = $this->collectCharsets($css);
215 1
            }
216
217
            $imports = $this->collectImports($css);
218
            $fonts = $this->collectFonts($css);
219
220
            file_put_contents($resultFile, $charsets . $imports . $fonts . $css);
221
222 2
            if (false !== $this->file_mode) {
223
                @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...
224
            }
225
        }
226
227
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
228
229
        $this->cssFiles[$file] = helpers\Html::cssFile($file);
230 2
    }
231
232
    /**
233
     * @return self
234
     */
235 2
    protected function minifyJS()
236
    {
237 2
        if (!empty($this->jsFiles)) {
238 2
            $jsFiles = $this->jsFiles;
239
240
            foreach ($jsFiles as $position => $files) {
241
                if (false === in_array($position, $this->js_position, true)) {
242 2
                    $this->jsFiles[$position] = [];
243
                    foreach ($files as $file => $html) {
244 2
                        $this->jsFiles[$position][$file] = $html;
245
                    }
246
                } else {
247 2
                    $this->jsFiles[$position] = [];
248
249 2
                    $toMinify = [];
250
251 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...
252
                        if ($this->thisFileNeedMinify($file, $html)) {
253 2
                            $toMinify[$file] = $html;
254
                        } else {
255 2
                            if (!empty($toMinify)) {
256
                                $this->processMinifyJs($position, $toMinify);
257
258
                                $toMinify = [];
259
                            }
260
261 2
                            $this->jsFiles[$position][$file] = $html;
262 2
                        }
263
                    }
264
265 2
                    if (!empty($toMinify)) {
266
                        $this->processMinifyJs($position, $toMinify);
267
                    }
268
269 2
                    unset($toMinify);
270 2
                }
271
            }
272
        }
273
274 2
        return $this;
275
    }
276
277
    /**
278
     * @param integer $position
279
     * @param array $files
280
     */
281 2
    protected function processMinifyJs($position, $files)
282
    {
283
        $resultFile = sprintf('%s/%s.js', $this->minify_path, $this->_getSummaryFilesHash($files));
284
        if (!file_exists($resultFile)) {
285 2
            $js = '';
286
            foreach ($files as $file => $html) {
287
                $file = $this->getAbsoluteFilePath($file);
288
                $js .= file_get_contents($file) . ';' . PHP_EOL;
289
            }
290
291
            $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...
292
293
            $compressedJs = (new \JSMin($js))
294
                ->min();
295
296
            file_put_contents($resultFile, $compressedJs);
297
298 2
            if (false !== $this->file_mode) {
299
                @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...
300
            }
301
        }
302
303
        $file = sprintf('%s%s', \Yii::getAlias($this->web_path), str_replace(\Yii::getAlias($this->base_path), '', $resultFile));
304
305
        $this->jsFiles[$position][$file] = helpers\Html::jsFile($file);
306 2
    }
307
308
    /**
309
     * @param string $url
310
     * @param boolean $checkSlash
311
     * @return bool
312
     */
313
    protected function isUrl($url, $checkSlash = true)
314
    {
315
        $regexp = '#^(' . implode('|', $this->schemas) . ')#is';
316
        if ($checkSlash) {
317
            $regexp = '#^(/|\\\\|' . implode('|', $this->schemas) . ')#is';
318
        }
319
320
        return (bool)preg_match($regexp, $url);
321
    }
322
323
    /**
324
     * @param string $string
325
     * @return bool
326
     */
327
    protected function isContainsConditionalComment($string)
328
    {
329
        return strpos($string, '<![endif]-->') !== false;
330
    }
331
332
    /**
333
     * @param string $file
334
     * @param string $html
335
     * @return bool
336
     */
337
    protected function thisFileNeedMinify($file, $html)
338
    {
339
        return !$this->isUrl($file, false) && !$this->isContainsConditionalComment($html);
340
    }
341
342
    /**
343
     * @param string $code
344
     * @return string
345
     */
346 1
    protected function collectCharsets(&$code)
347
    {
348
        return $this->_collect($code, '|\@charset[^;]+|is', function ($string) {
349 1
            return $string . ';';
350
        });
351
    }
352
353
    /**
354
     * @param string $code
355
     * @return string
356
     */
357
    protected function collectImports(&$code)
358
    {
359
        return $this->_collect($code, '|\@import[^;]+|is', function ($string) {
360
            return $string . ';';
361
        });
362
    }
363
364
    /**
365
     * @param string $code
366
     * @return string
367
     */
368
    protected function collectFonts(&$code)
369
    {
370
        return $this->_collect($code, '|\@font-face\{[^}]+\}|is', function ($string) {
371
            return $string;
372
        });
373
    }
374
375
    /**
376
     * @param string $code
377
     */
378 2
    protected function removeCssComments(&$code)
379
    {
380 2
        if (true === $this->removeComments) {
381
            $code = preg_replace('#/\*(?:[^*]*(?:\*(?!/))*)*\*/#', '', $code);
382
        }
383 2
    }
384
385
    /**
386
     * @param string $code
387
     */
388 2
    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...
389
    {
390
        // @todo
391 2
        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...
392
            //$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...
393
        }
394 2
    }
395
396
    /**
397
     * @param string $code
398
     */
399 2
    protected function expandImports(&$code)
400
    {
401 2
        if (true === $this->expand_imports) {
402
            preg_match_all('|\@import\s([^;]+);|is', str_replace('&amp;', '&', $code), $m);
403 1
            if (!empty($m[0])) {
404 1
                foreach ($m[0] as $k => $v) {
405
                    $import_url = $m[1][$k];
406
                    if (!empty($import_url)) {
407
                        $import_content = $this->_getImportContent($import_url);
408
                        if (!empty($import_content)) {
409
                            $code = str_replace($m[0][$k], $import_content, $code);
410
                        }
411
                    }
412
                }
413
            }
414
        }
415 2
    }
416
417
    /**
418
     * @param string $url
419
     * @return null|string
420
     */
421
    protected function _getImportContent($url)
422
    {
423
        $result = null;
424
425
        if ('url(' === helpers\StringHelper::byteSubstr($url, 0, 4)) {
426
            $url = str_replace(['url(\'', 'url("', 'url(', '\')', '")', ')'], '', $url);
427
428
            if (helpers\StringHelper::byteSubstr($url, 0, 2) === '//') {
429
                $url = preg_replace('|^//|', 'http://', $url, 1);
430
            }
431
432
            if (!empty($url)) {
433
                $result = file_get_contents($url);
434
            }
435
        }
436
437
        return $result;
438
    }
439
440
    /**
441
     * @param string $code
442
     * @param string $pattern
443
     * @param callable $handler
444
     * @return string
445
     */
446
    protected function _collect(&$code, $pattern, $handler)
447
    {
448
        $result = '';
449
450
        preg_match_all($pattern, $code, $m);
451
        foreach ($m[0] as $string) {
452
            $string = $handler($string);
453
            $code = str_replace($string, '', $code);
454
455
            $result .= $string . PHP_EOL;
456
        }
457
458
        return $result;
459
    }
460
461
    /**
462
     * @param string $file
463
     * @return string
464
     */
465
    protected function getAbsoluteFilePath($file)
466
    {
467
        return \Yii::getAlias($this->base_path) . str_replace(\Yii::getAlias($this->web_path), '', $file);
468
    }
469
470
    /**
471
     * @param array $files
472
     * @return string
473
     */
474
    protected function _getSummaryFilesHash($files)
475
    {
476
        $result = '';
477
        foreach ($files as $file => $html) {
478
            $path = $this->getAbsoluteFilePath($file);
479
480
            if ($this->thisFileNeedMinify($file, $html) && file_exists($path)) {
481
                $result .= sha1_file($path);
482
            }
483
        }
484
485
        return sha1($result);
486
    }
487
}