Completed
Push — 2.1 ( e31061...7c8525 )
by Carsten
75:33 queued 71:54
created

AssetController::saveTargets()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 43
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 5.0018

Importance

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