Passed
Branch master (2fd39c)
by Revin
02:26
created

View::_getSummaryFilesHash()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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