Completed
Push — master ( 49564a...c92cef )
by Dmitry
30s
created

AssetController::getAssetManager()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

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