Completed
Push — 2.1 ( 28b26f...4d9204 )
by Alexander
10:53
created

AssetController::loadTargets()   C

Complexity

Conditions 14
Paths 64

Size

Total Lines 56
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 15.42

Importance

Changes 0
Metric Value
dl 0
loc 56
ccs 25
cts 31
cp 0.8065
rs 6.6598
c 0
b 0
f 0
cc 14
eloc 33
nc 64
nop 2
crap 15.42

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\controllers;
9
10
use Yii;
11
use yii\console\Controller;
12
use yii\console\Exception;
13
use yii\console\ExitCode;
14
use yii\helpers\Console;
15
use yii\helpers\FileHelper;
16
use yii\helpers\VarDumper;
17
use yii\web\AssetBundle;
18
use yii\web\AssetManager;
19
20
/**
21
 * Allows you to combine and compress your JavaScript and CSS files.
22
 *
23
 * Usage:
24
 *
25
 * 1. Create a configuration file using the `template` action:
26
 *
27
 *    yii asset/template /path/to/myapp/config.php
28
 *
29
 * 2. Edit the created config file, adjusting it for your web application needs.
30
 * 3. Run the 'compress' action, using created config:
31
 *
32
 *    yii asset /path/to/myapp/config.php /path/to/myapp/config/assets_compressed.php
33
 *
34
 * 4. Adjust your web application config to use compressed assets.
35
 *
36
 * Note: in the console environment some [path aliases](guide:concept-aliases) like `@webroot` and `@web` may not exist,
37
 * so corresponding paths inside the configuration should be specified directly.
38
 *
39
 * Note: by default this command relies on an external tools to perform actual files compression,
40
 * check [[jsCompressor]] and [[cssCompressor]] for more details.
41
 *
42
 * @property AssetManager $assetManager Asset manager instance. Note that the type of this property
43
 * differs in getter and setter. See [[getAssetManager()]] and [[setAssetManager()]] for details.
44
 *
45
 * @author Qiang Xue <[email protected]>
46
 * @author Paul Klimov <[email protected]>
47
 * @since 2.0
48
 */
49
class AssetController extends Controller
50
{
51
    /**
52
     * @var string controller default action ID.
53
     */
54
    public $defaultAction = 'compress';
55
    /**
56
     * @var array list of asset bundles to be compressed.
57
     */
58
    public $bundles = [];
59
    /**
60
     * @var array list of asset bundles, which represents output compressed files.
61
     * You can specify the name of the output compressed file using 'css' and 'js' keys:
62
     * For example:
63
     *
64
     * ```php
65
     * 'app\config\AllAsset' => [
66
     *     'js' => 'js/all-{hash}.js',
67
     *     'css' => 'css/all-{hash}.css',
68
     *     'depends' => [ ... ],
69
     * ]
70
     * ```
71
     *
72
     * File names can contain placeholder "{hash}", which will be filled by the hash of the resulting file.
73
     *
74
     * You may specify several target bundles in order to compress different groups of assets.
75
     * In this case you should use 'depends' key to specify, which bundles should be covered with particular
76
     * target bundle. You may leave 'depends' to be empty for single bundle, which will compress all remaining
77
     * bundles in this case.
78
     * For example:
79
     *
80
     * ```php
81
     * 'allShared' => [
82
     *     'js' => 'js/all-shared-{hash}.js',
83
     *     'css' => 'css/all-shared-{hash}.css',
84
     *     'depends' => [
85
     *         // Include all assets shared between 'backend' and 'frontend'
86
     *         \yii\web\YiiAsset::class,
87
     *         \app\assets\SharedAsset::class,
88
     *     ],
89
     * ],
90
     * 'allBackEnd' => [
91
     *     'js' => 'js/all-{hash}.js',
92
     *     'css' => 'css/all-{hash}.css',
93
     *     'depends' => [
94
     *         // Include only 'backend' assets:
95
     *         \app\assets\AdminAsset::class
96
     *     ],
97
     * ],
98
     * 'allFrontEnd' => [
99
     *     'js' => 'js/all-{hash}.js',
100
     *     'css' => 'css/all-{hash}.css',
101
     *     'depends' => [], // Include all remaining assets
102
     * ],
103
     * ```
104
     */
105
    public $targets = [];
106
    /**
107
     * @var string|callable JavaScript file compressor.
108
     * If a string, it is treated as shell command template, which should contain
109
     * placeholders {from} - source file name - and {to} - output file name.
110
     * Otherwise, it is treated as PHP callback, which should perform the compression.
111
     *
112
     * Default value relies on usage of "Closure Compiler"
113
     * @see https://developers.google.com/closure/compiler/
114
     */
115
    public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}';
116
    /**
117
     * @var string|callable CSS file compressor.
118
     * If a string, it is treated as shell command template, which should contain
119
     * placeholders {from} - source file name - and {to} - output file name.
120
     * Otherwise, it is treated as PHP callback, which should perform the compression.
121
     *
122
     * Default value relies on usage of "YUI Compressor"
123
     * @see https://github.com/yui/yuicompressor/
124
     */
125
    public $cssCompressor = 'java -jar yuicompressor.jar --type css {from} -o {to}';
126
    /**
127
     * @var bool whether to delete asset source files after compression.
128
     * This option affects only those bundles, which have [[\yii\web\AssetBundle::sourcePath]] is set.
129
     * @since 2.0.10
130
     */
131
    public $deleteSource = false;
132
133
    /**
134
     * @var array|AssetManager [[AssetManager]] instance or its array configuration, which will be used
135
     * for assets processing.
136
     */
137
    private $_assetManager = [];
138
139
140
    /**
141
     * Returns the asset manager instance.
142
     * @throws \yii\console\Exception on invalid configuration.
143
     * @return \yii\web\AssetManager asset manager instance.
144
     */
145 5
    public function getAssetManager()
146
    {
147 5
        if (!is_object($this->_assetManager)) {
148 5
            $options = $this->_assetManager;
149 5
            if (empty($options['class'])) {
150 5
                $options['class'] = AssetManager::class;
151
            }
152 5
            if (!isset($options['basePath'])) {
153
                throw new Exception("Please specify 'basePath' for the 'assetManager' option.");
154
            }
155 5
            if (!isset($options['baseUrl'])) {
156
                throw new Exception("Please specify 'baseUrl' for the 'assetManager' option.");
157
            }
158
159 5
            if (!isset($options['forceCopy'])) {
160 5
                $options['forceCopy'] = true;
161
            }
162
163 5
            $this->_assetManager = Yii::createObject($options);
164
        }
165
166 5
        return $this->_assetManager;
167
    }
168
169
    /**
170
     * Sets asset manager instance or configuration.
171
     * @param AssetManager|array $assetManager asset manager instance or its array configuration.
172
     * @throws Exception on invalid argument type.
173
     */
174 5
    public function setAssetManager($assetManager)
175
    {
176 5
        if (is_scalar($assetManager)) {
177
            throw new Exception('"' . get_class($this) . '::assetManager" should be either object or array - "' . gettype($assetManager) . '" given.');
178
        }
179 5
        $this->_assetManager = $assetManager;
180 5
    }
181
182
    /**
183
     * Combines and compresses the asset files according to the given configuration.
184
     * During the process new asset bundle configuration file will be created.
185
     * You should replace your original asset bundle configuration with this file in order to use compressed files.
186
     * @param string $configFile configuration file name.
187
     * @param string $bundleFile output asset bundles configuration file name.
188
     */
189 5
    public function actionCompress($configFile, $bundleFile)
190
    {
191 5
        $this->loadConfiguration($configFile);
192 5
        $bundles = $this->loadBundles($this->bundles);
193 4
        $targets = $this->loadTargets($this->targets, $bundles);
194 4
        foreach ($targets as $name => $target) {
195 4
            $this->stdout("Creating output bundle '{$name}':\n");
196 4
            if (!empty($target->js)) {
197 4
                $this->buildTarget($target, 'js', $bundles);
198
            }
199 4
            if (!empty($target->css)) {
200 4
                $this->buildTarget($target, 'css', $bundles);
201
            }
202 4
            $this->stdout("\n");
203
        }
204
205 4
        $targets = $this->adjustDependency($targets, $bundles);
206 4
        $this->saveTargets($targets, $bundleFile);
207
208 4
        if ($this->deleteSource) {
209 1
            $this->deletePublishedAssets($bundles);
210
        }
211 4
    }
212
213
    /**
214
     * Applies configuration from the given file to self instance.
215
     * @param string $configFile configuration file name.
216
     * @throws \yii\console\Exception on failure.
217
     */
218 5
    protected function loadConfiguration($configFile)
219
    {
220 5
        $this->stdout("Loading configuration from '{$configFile}'...\n");
221 5
        $config = require $configFile;
222 5
        foreach ($config as $name => $value) {
223 5
            if (property_exists($this, $name) || $this->canSetProperty($name)) {
224 5
                $this->$name = $value;
225
            } else {
226 5
                throw new Exception("Unknown configuration option: $name");
227
            }
228
        }
229
230 5
        $this->getAssetManager(); // check if asset manager configuration is correct
231 5
    }
232
233
    /**
234
     * Creates full list of source asset bundles.
235
     * @param string[] $bundles list of asset bundle names
236
     * @return \yii\web\AssetBundle[] list of source asset bundles.
237
     */
238 5
    protected function loadBundles($bundles)
239
    {
240 5
        $this->stdout("Collecting source bundles information...\n");
241
242 5
        $am = $this->getAssetManager();
243 5
        $result = [];
244 5
        foreach ($bundles as $name) {
245 5
            $result[$name] = $am->getBundle($name);
246
        }
247 5
        foreach ($result as $bundle) {
248 5
            $this->loadDependency($bundle, $result);
249
        }
250
251 4
        return $result;
252
    }
253
254
    /**
255
     * Loads asset bundle dependencies recursively.
256
     * @param \yii\web\AssetBundle $bundle bundle instance
257
     * @param array $result already loaded bundles list.
258
     * @throws Exception on failure.
259
     */
260 5
    protected function loadDependency($bundle, &$result)
261
    {
262 5
        $am = $this->getAssetManager();
263 5
        foreach ($bundle->depends as $name) {
264 2
            if (!isset($result[$name])) {
265 2
                $dependencyBundle = $am->getBundle($name);
266 2
                $result[$name] = false;
267 2
                $this->loadDependency($dependencyBundle, $result);
268 1
                $result[$name] = $dependencyBundle;
269 1
            } elseif ($result[$name] === false) {
270 2
                throw new Exception("A circular dependency is detected for bundle '{$name}': " . $this->composeCircularDependencyTrace($name, $result) . '.');
271
            }
272
        }
273 4
    }
274
275
    /**
276
     * Creates full list of output asset bundles.
277
     * @param array $targets output asset bundles configuration.
278
     * @param \yii\web\AssetBundle[] $bundles list of source asset bundles.
279
     * @return \yii\web\AssetBundle[] list of output asset bundles.
280
     * @throws Exception on failure.
281
     */
282 4
    protected function loadTargets($targets, $bundles)
283
    {
284
        // build the dependency order of bundles
285 4
        $registered = [];
286 4
        foreach ($bundles as $name => $bundle) {
287 4
            $this->registerBundle($bundles, $name, $registered);
288
        }
289 4
        $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1));
290
291
        // fill up the target which has empty 'depends'.
292 4
        $referenced = [];
293 4
        foreach ($targets as $name => $target) {
294 4
            if (empty($target['depends'])) {
295 4
                if (!isset($all)) {
296 4
                    $all = $name;
297
                } else {
298 4
                    throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name");
299
                }
300
            } else {
301
                foreach ($target['depends'] as $bundle) {
302
                    if (!isset($referenced[$bundle])) {
303
                        $referenced[$bundle] = $name;
304
                    } else {
305 4
                        throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time.");
306
                    }
307
                }
308
            }
309
        }
310 4
        if (isset($all)) {
311 4
            $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced));
312
        }
313
314
        // adjust the 'depends' order for each target according to the dependency order of bundles
315
        // create an AssetBundle object for each target
316 4
        foreach ($targets as $name => $target) {
317 4
            if (!isset($target['basePath'])) {
318
                throw new Exception("Please specify 'basePath' for the '$name' target.");
319
            }
320 4
            if (!isset($target['baseUrl'])) {
321
                throw new Exception("Please specify 'baseUrl' for the '$name' target.");
322
            }
323 4
            usort($target['depends'], function ($a, $b) use ($bundleOrders) {
324 1
                if ($bundleOrders[$a] == $bundleOrders[$b]) {
325
                    return 0;
326
                }
327
328 1
                return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1;
329 4
            });
330 4
            if (!isset($target['class'])) {
331 4
                $target['class'] = $name;
332
            }
333 4
            $targets[$name] = Yii::createObject($target);
334
        }
335
336 4
        return $targets;
337
    }
338
339
    /**
340
     * Builds output asset bundle.
341
     * @param \yii\web\AssetBundle $target output asset bundle
342
     * @param string $type either 'js' or 'css'.
343
     * @param \yii\web\AssetBundle[] $bundles source asset bundles.
344
     * @throws Exception on failure.
345
     */
346 4
    protected function buildTarget($target, $type, $bundles)
347
    {
348 4
        $inputFiles = [];
349 4
        foreach ($target->depends as $name) {
350 4
            if (isset($bundles[$name])) {
351 4
                if (!$this->isBundleExternal($bundles[$name])) {
352 3
                    foreach ($bundles[$name]->$type as $file) {
353 3
                        if (is_array($file)) {
354
                            $inputFiles[] = $bundles[$name]->basePath . '/' . $file[0];
355
                        } else {
356 4
                            $inputFiles[] = $bundles[$name]->basePath . '/' . $file;
357
                        }
358
                    }
359
                }
360
            } else {
361 4
                throw new Exception("Unknown bundle: '{$name}'");
362
            }
363
        }
364
365 4
        if (empty($inputFiles)) {
366 1
            $target->$type = [];
367
        } else {
368 3
            FileHelper::createDirectory($target->basePath, $this->getAssetManager()->dirMode);
369 3
            $tempFile = $target->basePath . '/' . strtr($target->$type, ['{hash}' => 'temp']);
370
371 3
            if ($type === 'js') {
372 3
                $this->compressJsFiles($inputFiles, $tempFile);
373
            } else {
374 3
                $this->compressCssFiles($inputFiles, $tempFile);
375
            }
376
377 3
            $targetFile = strtr($target->$type, ['{hash}' => md5_file($tempFile)]);
378 3
            $outputFile = $target->basePath . '/' . $targetFile;
379 3
            rename($tempFile, $outputFile);
380 3
            $target->$type = [$targetFile];
381
        }
382 4
    }
383
384
    /**
385
     * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones.
386
     * @param \yii\web\AssetBundle[] $targets output asset bundles.
387
     * @param \yii\web\AssetBundle[] $bundles source asset bundles.
388
     * @return \yii\web\AssetBundle[] output asset bundles.
389
     */
390 4
    protected function adjustDependency($targets, $bundles)
391
    {
392 4
        $this->stdout("Creating new bundle configuration...\n");
393
394 4
        $map = [];
395 4
        foreach ($targets as $name => $target) {
396 4
            foreach ($target->depends as $bundle) {
397 4
                $map[$bundle] = $name;
398
            }
399
        }
400
401 4
        foreach ($targets as $name => $target) {
402 4
            $depends = [];
403 4
            foreach ($target->depends as $bn) {
404 4
                foreach ($bundles[$bn]->depends as $bundle) {
405 4
                    $depends[$map[$bundle]] = true;
406
                }
407
            }
408 4
            unset($depends[$name]);
409 4
            $target->depends = array_keys($depends);
410
        }
411
412
        // detect possible circular dependencies
413 4
        foreach ($targets as $name => $target) {
414 4
            $registered = [];
415 4
            $this->registerBundle($targets, $name, $registered);
416
        }
417
418 4
        foreach ($map as $bundle => $target) {
419 4
            $sourceBundle = $bundles[$bundle];
420 4
            $depends = $sourceBundle->depends;
421 4
            if (!$this->isBundleExternal($sourceBundle)) {
422 3
                $depends[] = $target;
423
            }
424 4
            $targetBundle = clone $sourceBundle;
425 4
            $targetBundle->depends = $depends;
426 4
            $targets[$bundle] = $targetBundle;
427
        }
428
429 4
        return $targets;
430
    }
431
432
    /**
433
     * Registers asset bundles including their dependencies.
434
     * @param \yii\web\AssetBundle[] $bundles asset bundles list.
435
     * @param string $name bundle name.
436
     * @param array $registered stores already registered names.
437
     * @throws Exception if circular dependency is detected.
438
     */
439 4
    protected function registerBundle($bundles, $name, &$registered)
440
    {
441 4
        if (!isset($registered[$name])) {
442 4
            $registered[$name] = false;
443 4
            $bundle = $bundles[$name];
444 4
            foreach ($bundle->depends as $depend) {
445 1
                $this->registerBundle($bundles, $depend, $registered);
446
            }
447 4
            unset($registered[$name]);
448 4
            $registered[$name] = $bundle;
449 1
        } elseif ($registered[$name] === false) {
450
            throw new Exception("A circular dependency is detected for target '{$name}': " . $this->composeCircularDependencyTrace($name, $registered) . '.');
451
        }
452 4
    }
453
454
    /**
455
     * Saves new asset bundles configuration.
456
     * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved.
457
     * @param string $bundleFile output file name.
458
     * @throws \yii\console\Exception on failure.
459
     */
460 4
    protected function saveTargets($targets, $bundleFile)
461
    {
462 4
        $array = [];
463 4
        foreach ($targets as $name => $target) {
464 4
            if (isset($this->targets[$name])) {
465 4
                $array[$name] = array_merge($this->targets[$name], [
466 4
                    'class' => get_class($target),
467
                    'sourcePath' => null,
468 4
                    'basePath' => $this->targets[$name]['basePath'],
469 4
                    'baseUrl' => $this->targets[$name]['baseUrl'],
470 4
                    'js' => $target->js,
471 4
                    'css' => $target->css,
472
                    'depends' => [],
473
                ]);
474
            } else {
475 4
                if ($this->isBundleExternal($target)) {
476 2
                    $array[$name] = $this->composeBundleConfig($target);
477
                } else {
478 3
                    $array[$name] = [
479 3
                        'sourcePath' => null,
480
                        'js' => [],
481
                        'css' => [],
482 4
                        'depends' => $target->depends,
483
                    ];
484
                }
485
            }
486
        }
487 4
        $array = VarDumper::export($array);
488 4
        $version = date('Y-m-d H:i:s', time());
489
        $bundleFileContent = <<<EOD
490
<?php
491
/**
492 4
 * This file is generated by the "yii {$this->id}" command.
493
 * DO NOT MODIFY THIS FILE DIRECTLY.
494 4
 * @version {$version}
495
 */
496 4
return {$array};
497
EOD;
498 4
        if (!file_put_contents($bundleFile, $bundleFileContent)) {
499
            throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'.");
500
        }
501 4
        $this->stdout("Output bundle configuration created at '{$bundleFile}'.\n", Console::FG_GREEN);
502 4
    }
503
504
    /**
505
     * Compresses given JavaScript files and combines them into the single one.
506
     * @param array $inputFiles list of source file names.
507
     * @param string $outputFile output file name.
508
     * @throws \yii\console\Exception on failure
509
     */
510 3
    protected function compressJsFiles($inputFiles, $outputFile)
511
    {
512 3
        if (empty($inputFiles)) {
513
            return;
514
        }
515 3
        $this->stdout("  Compressing JavaScript files...\n");
516 3
        if (is_string($this->jsCompressor)) {
517 3
            $tmpFile = $outputFile . '.tmp';
518 3
            $this->combineJsFiles($inputFiles, $tmpFile);
519 3
            $this->stdout(shell_exec(strtr($this->jsCompressor, [
520 3
                '{from}' => escapeshellarg($tmpFile),
521 3
                '{to}' => escapeshellarg($outputFile),
522
            ])));
523 3
            @unlink($tmpFile);
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...
524
        } else {
525
            call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile);
526
        }
527 3
        if (!file_exists($outputFile)) {
528
            throw new Exception("Unable to compress JavaScript files into '{$outputFile}'.");
529
        }
530 3
        $this->stdout("  JavaScript files compressed into '{$outputFile}'.\n");
531 3
    }
532
533
    /**
534
     * Compresses given CSS files and combines them into the single one.
535
     * @param array $inputFiles list of source file names.
536
     * @param string $outputFile output file name.
537
     * @throws \yii\console\Exception on failure
538
     */
539 3
    protected function compressCssFiles($inputFiles, $outputFile)
540
    {
541 3
        if (empty($inputFiles)) {
542
            return;
543
        }
544 3
        $this->stdout("  Compressing CSS files...\n");
545 3
        if (is_string($this->cssCompressor)) {
546 3
            $tmpFile = $outputFile . '.tmp';
547 3
            $this->combineCssFiles($inputFiles, $tmpFile);
548 3
            $this->stdout(shell_exec(strtr($this->cssCompressor, [
549 3
                '{from}' => escapeshellarg($tmpFile),
550 3
                '{to}' => escapeshellarg($outputFile),
551
            ])));
552 3
            @unlink($tmpFile);
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...
553
        } else {
554
            call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile);
555
        }
556 3
        if (!file_exists($outputFile)) {
557
            throw new Exception("Unable to compress CSS files into '{$outputFile}'.");
558
        }
559 3
        $this->stdout("  CSS files compressed into '{$outputFile}'.\n");
560 3
    }
561
562
    /**
563
     * Combines JavaScript files into a single one.
564
     * @param array $inputFiles source file names.
565
     * @param string $outputFile output file name.
566
     * @throws \yii\console\Exception on failure.
567
     */
568 3
    public function combineJsFiles($inputFiles, $outputFile)
569
    {
570 3
        $content = '';
571 3
        foreach ($inputFiles as $file) {
572 3
            $content .= "/*** BEGIN FILE: $file ***/\n"
573 3
                . file_get_contents($file)
574 3
                . "/*** END FILE: $file ***/\n";
575
        }
576 3
        if (!file_put_contents($outputFile, $content)) {
577
            throw new Exception("Unable to write output JavaScript file '{$outputFile}'.");
578
        }
579 3
    }
580
581
    /**
582
     * Combines CSS files into a single one.
583
     * @param array $inputFiles source file names.
584
     * @param string $outputFile output file name.
585
     * @throws \yii\console\Exception on failure.
586
     */
587 3
    public function combineCssFiles($inputFiles, $outputFile)
588
    {
589 3
        $content = '';
590 3
        $outputFilePath = dirname($this->findRealPath($outputFile));
591 3
        foreach ($inputFiles as $file) {
592 3
            $content .= "/*** BEGIN FILE: $file ***/\n"
593 3
                . $this->adjustCssUrl(file_get_contents($file), dirname($this->findRealPath($file)), $outputFilePath)
594 3
                . "/*** END FILE: $file ***/\n";
595
        }
596 3
        if (!file_put_contents($outputFile, $content)) {
597
            throw new Exception("Unable to write output CSS file '{$outputFile}'.");
598
        }
599 3
    }
600
601
    /**
602
     * Adjusts CSS content allowing URL references pointing to the original resources.
603
     * @param string $cssContent source CSS content.
604
     * @param string $inputFilePath input CSS file name.
605
     * @param string $outputFilePath output CSS file name.
606
     * @return string adjusted CSS content.
607
     */
608 16
    protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath)
609
    {
610 16
        $inputFilePath = str_replace('\\', '/', $inputFilePath);
611 16
        $outputFilePath = str_replace('\\', '/', $outputFilePath);
612
613 16
        $sharedPathParts = [];
614 16
        $inputFilePathParts = explode('/', $inputFilePath);
615 16
        $inputFilePathPartsCount = count($inputFilePathParts);
616 16
        $outputFilePathParts = explode('/', $outputFilePath);
617 16
        $outputFilePathPartsCount = count($outputFilePathParts);
618 16
        for ($i = 0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) {
619 16
            if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) {
620 16
                $sharedPathParts[] = $inputFilePathParts[$i];
621
            } else {
622 13
                break;
623
            }
624
        }
625 16
        $sharedPath = implode('/', $sharedPathParts);
626
627 16
        $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/');
628 16
        $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/');
629 16
        if (empty($inputFileRelativePath)) {
630 1
            $inputFileRelativePathParts = [];
631
        } else {
632 15
            $inputFileRelativePathParts = explode('/', $inputFileRelativePath);
633
        }
634 16
        if (empty($outputFileRelativePath)) {
635 3
            $outputFileRelativePathParts = [];
636
        } else {
637 13
            $outputFileRelativePathParts = explode('/', $outputFileRelativePath);
638
        }
639
640 16
        $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) {
641 13
            $fullMatch = $matches[0];
642 13
            $inputUrl = $matches[1];
643
644 13
            if (strpos($inputUrl, '/') === 0 || strpos($inputUrl, '#') === 0 || preg_match('/^https?:\/\//i', $inputUrl) || preg_match('/^data:/i', $inputUrl)) {
645 5
                return $fullMatch;
646
            }
647 8
            if ($inputFileRelativePathParts === $outputFileRelativePathParts) {
648 1
                return $fullMatch;
649
            }
650
651 7
            if (empty($outputFileRelativePathParts)) {
652 1
                $outputUrlParts = [];
653
            } else {
654 6
                $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..');
655
            }
656 7
            $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts);
657
658 7
            if (strpos($inputUrl, '/') !== false) {
659 4
                $inputUrlParts = explode('/', $inputUrl);
660 4
                foreach ($inputUrlParts as $key => $inputUrlPart) {
661 4
                    if ($inputUrlPart === '..') {
662 4
                        array_pop($outputUrlParts);
663 4
                        unset($inputUrlParts[$key]);
664
                    }
665
                }
666 4
                $outputUrlParts[] = implode('/', $inputUrlParts);
667
            } else {
668 3
                $outputUrlParts[] = $inputUrl;
669
            }
670 7
            $outputUrl = implode('/', $outputUrlParts);
671
672 7
            return str_replace($inputUrl, $outputUrl, $fullMatch);
673 16
        };
674
675 16
        $cssContent = preg_replace_callback('/url\(["\']?([^)^"^\']*)["\']?\)/i', $callback, $cssContent);
676
677 16
        return $cssContent;
678
    }
679
680
    /**
681
     * Creates template of configuration file for [[actionCompress]].
682
     * @param string $configFile output file name.
683
     * @return int CLI exit code
684
     * @throws \yii\console\Exception on failure.
685
     */
686 1
    public function actionTemplate($configFile)
687
    {
688 1
        $jsCompressor = VarDumper::export($this->jsCompressor);
689 1
        $cssCompressor = VarDumper::export($this->cssCompressor);
690
691
        $template = <<<EOD
692
<?php
693
/**
694
 * Configuration file for the "yii asset" console command.
695
 */
696
697
// In the console environment, some path aliases may not exist. Please define these:
698
// Yii::setAlias('@webroot', __DIR__ . '/../web');
699
// Yii::setAlias('@web', '/');
700
701
return [
702
    // Adjust command/callback for JavaScript files compressing:
703 1
    'jsCompressor' => {$jsCompressor},
704
    // Adjust command/callback for CSS files compressing:
705 1
    'cssCompressor' => {$cssCompressor},
706
    // Whether to delete asset source after compression:
707
    'deleteSource' => false,
708
    // The list of asset bundles to compress:
709
    'bundles' => [
710
        // 'app\assets\AppAsset',
711
        // \yii\web\YiiAsset::class,
712
        // \yii\web\JqueryAsset::class,
713
    ],
714
    // Asset bundle for compression output:
715
    'targets' => [
716
        'all' => [
717
            'class' => \yii\web\AssetBundle::class,
718
            'basePath' => '@webroot/assets',
719
            'baseUrl' => '@web/assets',
720
            'js' => 'js/all-{hash}.js',
721
            'css' => 'css/all-{hash}.css',
722
        ],
723
    ],
724
    // Asset manager configuration:
725
    'assetManager' => [
726
        //'basePath' => '@webroot/assets',
727
        //'baseUrl' => '@web/assets',
728
    ],
729
];
730
EOD;
731 1
        if (file_exists($configFile)) {
732
            if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->confirm("File '{$...wish to overwrite it?") of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
733
                return ExitCode::OK;
734
            }
735
        }
736 1
        if (!file_put_contents($configFile, $template)) {
737
            throw new Exception("Unable to write template file '{$configFile}'.");
738
        }
739
740 1
        $this->stdout("Configuration file template created at '{$configFile}'.\n\n", Console::FG_GREEN);
741 1
        return ExitCode::OK;
742
    }
743
744
    /**
745
     * Returns canonicalized absolute pathname.
746
     * Unlike regular `realpath()` this method does not expand symlinks and does not check path existence.
747
     * @param string $path raw path
748
     * @return string canonicalized absolute pathname
749
     */
750 9
    private function findRealPath($path)
751
    {
752 9
        $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
753 9
        $pathParts = explode(DIRECTORY_SEPARATOR, $path);
754
755 9
        $realPathParts = [];
756 9
        foreach ($pathParts as $pathPart) {
757 9
            if ($pathPart === '..') {
758 4
                array_pop($realPathParts);
759
            } else {
760 9
                $realPathParts[] = $pathPart;
761
            }
762
        }
763 9
        return implode(DIRECTORY_SEPARATOR, $realPathParts);
764
    }
765
766
    /**
767
     * @param AssetBundle $bundle
768
     * @return bool whether asset bundle external or not.
769
     */
770 4
    private function isBundleExternal($bundle)
771
    {
772 4
        return empty($bundle->sourcePath) && empty($bundle->basePath);
773
    }
774
775
    /**
776
     * @param AssetBundle $bundle asset bundle instance.
777
     * @return array bundle configuration.
778
     */
779 2
    private function composeBundleConfig($bundle)
780
    {
781 2
        $config = Yii::getObjectVars($bundle);
782 2
        $config['class'] = get_class($bundle);
783 2
        return $config;
784
    }
785
786
    /**
787
     * Composes trace info for bundle circular dependency.
788
     * @param string $circularDependencyName name of the bundle, which have circular dependency
789
     * @param array $registered list of bundles registered while detecting circular dependency.
790
     * @return string bundle circular dependency trace string.
791
     */
792 1
    private function composeCircularDependencyTrace($circularDependencyName, array $registered)
793
    {
794 1
        $dependencyTrace = [];
795 1
        $startFound = false;
796 1
        foreach ($registered as $name => $value) {
797 1
            if ($name === $circularDependencyName) {
798 1
                $startFound = true;
799
            }
800 1
            if ($startFound && $value === false) {
801 1
                $dependencyTrace[] = $name;
802
            }
803
        }
804 1
        $dependencyTrace[] = $circularDependencyName;
805 1
        return implode(' -> ', $dependencyTrace);
806
    }
807
808
    /**
809
     * Deletes bundle asset files, which have been published from `sourcePath`.
810
     * @param \yii\web\AssetBundle[] $bundles asset bundles to be processed.
811
     * @since 2.0.10
812
     */
813 1
    private function deletePublishedAssets($bundles)
814
    {
815 1
        $this->stdout("Deleting source files...\n");
816
817 1
        if ($this->getAssetManager()->linkAssets) {
818
            $this->stdout("`AssetManager::linkAssets` option is enabled. Deleting of source files canceled.\n", Console::FG_YELLOW);
819
            return;
820
        }
821
822 1
        foreach ($bundles as $bundle) {
823 1
            if ($bundle->sourcePath !== null) {
824 1
                foreach ($bundle->js as $jsFile) {
825 1
                    @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $jsFile);
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...
826
                }
827 1
                foreach ($bundle->css as $cssFile) {
828 1
                    @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $cssFile);
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...
829
                }
830
            }
831
        }
832
833 1
        $this->stdout("Source files deleted.\n", Console::FG_GREEN);
834 1
    }
835
}
836