Issues (910)

framework/console/controllers/AssetController.php (1 issue)

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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
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](guide:concept-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 \yii\web\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',
86
     *         'app\assets\SharedAsset',
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'
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|\yii\web\AssetManager [[\yii\web\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 (!isset($options['class'])) {
149 5
                $options['class'] = 'yii\\web\\AssetManager';
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 \yii\web\AssetManager|array $assetManager asset manager instance or its array configuration.
171
     * @throws \yii\console\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
    }
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
    }
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
        $config = require $configFile;
221 5
        foreach ($config as $name => $value) {
222 5
            if (property_exists($this, $name) || $this->canSetProperty($name)) {
223 5
                $this->$name = $value;
224
            } else {
225
                throw new Exception("Unknown configuration option: $name");
226
            }
227
        }
228
229 5
        $this->getAssetManager(); // check if asset manager configuration is correct
230
    }
231
232
    /**
233
     * Creates full list of source asset bundles.
234
     * @param string[] $bundles list of asset bundle names
235
     * @return \yii\web\AssetBundle[] list of source asset bundles.
236
     */
237 5
    protected function loadBundles($bundles)
238
    {
239 5
        $this->stdout("Collecting source bundles information...\n");
240
241 5
        $am = $this->getAssetManager();
242 5
        $result = [];
243 5
        foreach ($bundles as $name) {
244 5
            $result[$name] = $am->getBundle($name);
245
        }
246 5
        foreach ($result as $bundle) {
247 5
            $this->loadDependency($bundle, $result);
248
        }
249
250 4
        return $result;
251
    }
252
253
    /**
254
     * Loads asset bundle dependencies recursively.
255
     * @param \yii\web\AssetBundle $bundle bundle instance
256
     * @param array $result already loaded bundles list.
257
     * @throws Exception on failure.
258
     */
259 5
    protected function loadDependency($bundle, &$result)
260
    {
261 5
        $am = $this->getAssetManager();
262 5
        foreach ($bundle->depends as $name) {
263 2
            if (!isset($result[$name])) {
264 2
                $dependencyBundle = $am->getBundle($name);
265 2
                $result[$name] = false;
266 2
                $this->loadDependency($dependencyBundle, $result);
267 1
                $result[$name] = $dependencyBundle;
268 1
            } elseif ($result[$name] === false) {
269 1
                throw new Exception("A circular dependency is detected for bundle '{$name}': " . $this->composeCircularDependencyTrace($name, $result) . '.');
270
            }
271
        }
272
    }
273
274
    /**
275
     * Creates full list of output asset bundles.
276
     * @param array $targets output asset bundles configuration.
277
     * @param \yii\web\AssetBundle[] $bundles list of source asset bundles.
278
     * @return \yii\web\AssetBundle[] list of output asset bundles.
279
     * @throws Exception on failure.
280
     */
281 4
    protected function loadTargets($targets, $bundles)
282
    {
283
        // build the dependency order of bundles
284 4
        $registered = [];
285 4
        foreach ($bundles as $name => $bundle) {
286 4
            $this->registerBundle($bundles, $name, $registered);
287
        }
288 4
        $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1));
289
290
        // fill up the target which has empty 'depends'.
291 4
        $referenced = [];
292 4
        foreach ($targets as $name => $target) {
293 4
            if (empty($target['depends'])) {
294 4
                if (!isset($all)) {
295 4
                    $all = $name;
296
                } else {
297 4
                    throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name");
298
                }
299
            } else {
300
                foreach ($target['depends'] as $bundle) {
301
                    if (!isset($referenced[$bundle])) {
302
                        $referenced[$bundle] = $name;
303
                    } else {
304
                        throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time.");
305
                    }
306
                }
307
            }
308
        }
309 4
        if (isset($all)) {
310 4
            $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced));
311
        }
312
313
        // adjust the 'depends' order for each target according to the dependency order of bundles
314
        // create an AssetBundle object for each target
315 4
        foreach ($targets as $name => $target) {
316 4
            if (!isset($target['basePath'])) {
317
                throw new Exception("Please specify 'basePath' for the '$name' target.");
318
            }
319 4
            if (!isset($target['baseUrl'])) {
320
                throw new Exception("Please specify 'baseUrl' for the '$name' target.");
321
            }
322 4
            usort($target['depends'], function ($a, $b) use ($bundleOrders) {
323 1
                if ($bundleOrders[$a] == $bundleOrders[$b]) {
324
                    return 0;
325
                }
326
327 1
                return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1;
328 4
            });
329 4
            if (!isset($target['class'])) {
330 4
                $target['class'] = $name;
331
            }
332 4
            $targets[$name] = Yii::createObject($target);
333
        }
334
335 4
        return $targets;
336
    }
337
338
    /**
339
     * Builds output asset bundle.
340
     * @param \yii\web\AssetBundle $target output asset bundle
341
     * @param string $type either 'js' or 'css'.
342
     * @param \yii\web\AssetBundle[] $bundles source asset bundles.
343
     * @throws Exception on failure.
344
     */
345 4
    protected function buildTarget($target, $type, $bundles)
346
    {
347 4
        $inputFiles = [];
348 4
        foreach ($target->depends as $name) {
349 4
            if (isset($bundles[$name])) {
350 4
                if (!$this->isBundleExternal($bundles[$name])) {
351 4
                    foreach ($bundles[$name]->$type as $file) {
352 3
                        if (is_array($file)) {
353
                            $inputFiles[] = $bundles[$name]->basePath . '/' . $file[0];
354
                        } else {
355 3
                            $inputFiles[] = $bundles[$name]->basePath . '/' . $file;
356
                        }
357
                    }
358
                }
359
            } else {
360
                throw new Exception("Unknown bundle: '{$name}'");
361
            }
362
        }
363
364 4
        if (empty($inputFiles)) {
365 1
            $target->$type = [];
366
        } else {
367 3
            FileHelper::createDirectory($target->basePath, $this->getAssetManager()->dirMode);
368 3
            $tempFile = $target->basePath . '/' . strtr($target->$type, ['{hash}' => 'temp']);
369
370 3
            if ($type === 'js') {
371 3
                $this->compressJsFiles($inputFiles, $tempFile);
372
            } else {
373 3
                $this->compressCssFiles($inputFiles, $tempFile);
374
            }
375
376 3
            $targetFile = strtr($target->$type, ['{hash}' => md5_file($tempFile)]);
377 3
            $outputFile = $target->basePath . '/' . $targetFile;
378 3
            rename($tempFile, $outputFile);
379 3
            $target->$type = [$targetFile];
380
        }
381
    }
382
383
    /**
384
     * Adjust dependencies between asset bundles in the way source bundles begin to depend on output ones.
385
     * @param \yii\web\AssetBundle[] $targets output asset bundles.
386
     * @param \yii\web\AssetBundle[] $bundles source asset bundles.
387
     * @return \yii\web\AssetBundle[] output asset bundles.
388
     */
389 4
    protected function adjustDependency($targets, $bundles)
390
    {
391 4
        $this->stdout("Creating new bundle configuration...\n");
392
393 4
        $map = [];
394 4
        foreach ($targets as $name => $target) {
395 4
            foreach ($target->depends as $bundle) {
396 4
                $map[$bundle] = $name;
397
            }
398
        }
399
400 4
        foreach ($targets as $name => $target) {
401 4
            $depends = [];
402 4
            foreach ($target->depends as $bn) {
403 4
                foreach ($bundles[$bn]->depends as $bundle) {
404 1
                    $depends[$map[$bundle]] = true;
405
                }
406
            }
407 4
            unset($depends[$name]);
408 4
            $target->depends = array_keys($depends);
409
        }
410
411
        // detect possible circular dependencies
412 4
        foreach ($targets as $name => $target) {
413 4
            $registered = [];
414 4
            $this->registerBundle($targets, $name, $registered);
415
        }
416
417 4
        foreach ($map as $bundle => $target) {
418 4
            $sourceBundle = $bundles[$bundle];
419 4
            $depends = $sourceBundle->depends;
420 4
            if (!$this->isBundleExternal($sourceBundle)) {
421 3
                $depends[] = $target;
422
            }
423 4
            $targetBundle = clone $sourceBundle;
424 4
            $targetBundle->depends = $depends;
425 4
            $targets[$bundle] = $targetBundle;
426
        }
427
428 4
        return $targets;
429
    }
430
431
    /**
432
     * Registers asset bundles including their dependencies.
433
     * @param \yii\web\AssetBundle[] $bundles asset bundles list.
434
     * @param string $name bundle name.
435
     * @param array $registered stores already registered names.
436
     * @throws Exception if circular dependency is detected.
437
     */
438 4
    protected function registerBundle($bundles, $name, &$registered)
439
    {
440 4
        if (!isset($registered[$name])) {
441 4
            $registered[$name] = false;
442 4
            $bundle = $bundles[$name];
443 4
            foreach ($bundle->depends as $depend) {
444 1
                $this->registerBundle($bundles, $depend, $registered);
445
            }
446 4
            unset($registered[$name]);
447 4
            $registered[$name] = $bundle;
448 1
        } elseif ($registered[$name] === false) {
449
            throw new Exception("A circular dependency is detected for target '{$name}': " . $this->composeCircularDependencyTrace($name, $registered) . '.');
450
        }
451
    }
452
453
    /**
454
     * Saves new asset bundles configuration.
455
     * @param \yii\web\AssetBundle[] $targets list of asset bundles to be saved.
456
     * @param string $bundleFile output file name.
457
     * @throws \yii\console\Exception on failure.
458
     */
459 4
    protected function saveTargets($targets, $bundleFile)
460
    {
461 4
        $array = [];
462 4
        foreach ($targets as $name => $target) {
463 4
            if (isset($this->targets[$name])) {
464 4
                $array[$name] = array_merge($this->targets[$name], [
465 4
                    'class' => get_class($target),
466 4
                    'sourcePath' => null,
467 4
                    'basePath' => $this->targets[$name]['basePath'],
468 4
                    'baseUrl' => $this->targets[$name]['baseUrl'],
469 4
                    'js' => $target->js,
470 4
                    'css' => $target->css,
471 4
                    'depends' => [],
472 4
                ]);
473
            } else {
474 4
                if ($this->isBundleExternal($target)) {
475 2
                    $array[$name] = $this->composeBundleConfig($target);
476
                } else {
477 3
                    $array[$name] = [
478 3
                        'sourcePath' => null,
479 3
                        'js' => [],
480 3
                        'css' => [],
481 3
                        'depends' => $target->depends,
482 3
                    ];
483
                }
484
            }
485
        }
486 4
        $array = VarDumper::export($array);
487 4
        $version = date('Y-m-d H:i:s');
488 4
        $bundleFileContent = <<<EOD
489 4
<?php
490
/**
491 4
 * This file is generated by the "yii {$this->id}" command.
492
 * DO NOT MODIFY THIS FILE DIRECTLY.
493 4
 * @version {$version}
494
 */
495 4
return {$array};
496 4
EOD;
497 4
        if (!file_put_contents($bundleFile, $bundleFileContent, LOCK_EX)) {
498
            throw new Exception("Unable to write output bundle configuration at '{$bundleFile}'.");
499
        }
500 4
        $this->stdout("Output bundle configuration created at '{$bundleFile}'.\n", Console::FG_GREEN);
501
    }
502
503
    /**
504
     * Compresses given JavaScript files and combines them into the single one.
505
     * @param array $inputFiles list of source file names.
506
     * @param string $outputFile output file name.
507
     * @throws \yii\console\Exception on failure
508
     */
509 3
    protected function compressJsFiles($inputFiles, $outputFile)
510
    {
511 3
        if (empty($inputFiles)) {
512
            return;
513
        }
514 3
        $this->stdout("  Compressing JavaScript files...\n");
515 3
        if (is_string($this->jsCompressor)) {
516 3
            $tmpFile = $outputFile . '.tmp';
517 3
            $this->combineJsFiles($inputFiles, $tmpFile);
518 3
            $this->stdout((string)shell_exec(strtr($this->jsCompressor, [
519 3
                '{from}' => escapeshellarg($tmpFile),
520 3
                '{to}' => escapeshellarg($outputFile),
521 3
            ])));
522 3
            @unlink($tmpFile);
523
        } else {
524
            call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile);
525
        }
526 3
        if (!file_exists($outputFile)) {
527
            throw new Exception("Unable to compress JavaScript files into '{$outputFile}'.");
528
        }
529 3
        $this->stdout("  JavaScript files compressed into '{$outputFile}'.\n");
530
    }
531
532
    /**
533
     * Compresses given CSS files and combines them into the single one.
534
     * @param array $inputFiles list of source file names.
535
     * @param string $outputFile output file name.
536
     * @throws \yii\console\Exception on failure
537
     */
538 3
    protected function compressCssFiles($inputFiles, $outputFile)
539
    {
540 3
        if (empty($inputFiles)) {
541
            return;
542
        }
543 3
        $this->stdout("  Compressing CSS files...\n");
544 3
        if (is_string($this->cssCompressor)) {
545 3
            $tmpFile = $outputFile . '.tmp';
546 3
            $this->combineCssFiles($inputFiles, $tmpFile);
547 3
            $this->stdout((string)shell_exec(strtr($this->cssCompressor, [
548 3
                '{from}' => escapeshellarg($tmpFile),
549 3
                '{to}' => escapeshellarg($outputFile),
550 3
            ])));
551 3
            @unlink($tmpFile);
552
        } else {
553
            call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile);
554
        }
555 3
        if (!file_exists($outputFile)) {
556
            throw new Exception("Unable to compress CSS files into '{$outputFile}'.");
557
        }
558 3
        $this->stdout("  CSS files compressed into '{$outputFile}'.\n");
559
    }
560
561
    /**
562
     * Combines JavaScript files into a single one.
563
     * @param array $inputFiles source file names.
564
     * @param string $outputFile output file name.
565
     * @throws \yii\console\Exception on failure.
566
     */
567 3
    public function combineJsFiles($inputFiles, $outputFile)
568
    {
569 3
        $content = '';
570 3
        foreach ($inputFiles as $file) {
571
            // Add a semicolon to source code if trailing semicolon missing.
572
            // Notice: It needs a new line before `;` to avoid affection of line comment. (// ...;)
573 3
            $fileContent = rtrim(file_get_contents($file));
574 3
            if (substr($fileContent, -1) !== ';') {
575 3
                $fileContent .= "\n;";
576
            }
577 3
            $content .= "/*** BEGIN FILE: $file ***/\n"
578 3
                . $fileContent . "\n"
579 3
                . "/*** END FILE: $file ***/\n";
580
        }
581 3
        if (!file_put_contents($outputFile, $content)) {
582
            throw new Exception("Unable to write output JavaScript file '{$outputFile}'.");
583
        }
584
    }
585
586
    /**
587
     * Combines CSS files into a single one.
588
     * @param array $inputFiles source file names.
589
     * @param string $outputFile output file name.
590
     * @throws \yii\console\Exception on failure.
591
     */
592 3
    public function combineCssFiles($inputFiles, $outputFile)
593
    {
594 3
        $content = '';
595 3
        $outputFilePath = dirname($this->findRealPath($outputFile));
596 3
        foreach ($inputFiles as $file) {
597 3
            $content .= "/*** BEGIN FILE: $file ***/\n"
598 3
                . $this->adjustCssUrl(file_get_contents($file), dirname($this->findRealPath($file)), $outputFilePath)
599 3
                . "/*** END FILE: $file ***/\n";
600
        }
601 3
        if (!file_put_contents($outputFile, $content)) {
602
            throw new Exception("Unable to write output CSS file '{$outputFile}'.");
603
        }
604
    }
605
606
    /**
607
     * Adjusts CSS content allowing URL references pointing to the original resources.
608
     * @param string $cssContent source CSS content.
609
     * @param string $inputFilePath input CSS file name.
610
     * @param string $outputFilePath output CSS file name.
611
     * @return string adjusted CSS content.
612
     */
613 16
    protected function adjustCssUrl($cssContent, $inputFilePath, $outputFilePath)
614
    {
615 16
        $inputFilePath = str_replace('\\', '/', $inputFilePath);
616 16
        $outputFilePath = str_replace('\\', '/', $outputFilePath);
617
618 16
        $sharedPathParts = [];
619 16
        $inputFilePathParts = explode('/', $inputFilePath);
620 16
        $inputFilePathPartsCount = count($inputFilePathParts);
621 16
        $outputFilePathParts = explode('/', $outputFilePath);
622 16
        $outputFilePathPartsCount = count($outputFilePathParts);
623 16
        for ($i = 0; $i < $inputFilePathPartsCount && $i < $outputFilePathPartsCount; $i++) {
624 16
            if ($inputFilePathParts[$i] == $outputFilePathParts[$i]) {
625 16
                $sharedPathParts[] = $inputFilePathParts[$i];
626
            } else {
627 13
                break;
628
            }
629
        }
630 16
        $sharedPath = implode('/', $sharedPathParts);
631
632 16
        $inputFileRelativePath = trim(str_replace($sharedPath, '', $inputFilePath), '/');
633 16
        $outputFileRelativePath = trim(str_replace($sharedPath, '', $outputFilePath), '/');
634 16
        if (empty($inputFileRelativePath)) {
635 1
            $inputFileRelativePathParts = [];
636
        } else {
637 15
            $inputFileRelativePathParts = explode('/', $inputFileRelativePath);
638
        }
639 16
        if (empty($outputFileRelativePath)) {
640 3
            $outputFileRelativePathParts = [];
641
        } else {
642 13
            $outputFileRelativePathParts = explode('/', $outputFileRelativePath);
643
        }
644
645 16
        $callback = function ($matches) use ($inputFileRelativePathParts, $outputFileRelativePathParts) {
646 13
            $fullMatch = $matches[0];
647 13
            $inputUrl = $matches[1];
648
649 13
            if (strncmp($inputUrl, '/', 1) === 0 || strncmp($inputUrl, '#', 1) === 0 || preg_match('/^https?:\/\//i', $inputUrl) || preg_match('/^data:/i', $inputUrl)) {
650 5
                return $fullMatch;
651
            }
652 8
            if ($inputFileRelativePathParts === $outputFileRelativePathParts) {
653 1
                return $fullMatch;
654
            }
655
656 7
            if (empty($outputFileRelativePathParts)) {
657 1
                $outputUrlParts = [];
658
            } else {
659 6
                $outputUrlParts = array_fill(0, count($outputFileRelativePathParts), '..');
660
            }
661 7
            $outputUrlParts = array_merge($outputUrlParts, $inputFileRelativePathParts);
662
663 7
            if (strpos($inputUrl, '/') !== false) {
664 4
                $inputUrlParts = explode('/', $inputUrl);
665 4
                foreach ($inputUrlParts as $key => $inputUrlPart) {
666 4
                    if ($inputUrlPart === '..') {
667 4
                        array_pop($outputUrlParts);
668 4
                        unset($inputUrlParts[$key]);
669
                    }
670
                }
671 4
                $outputUrlParts[] = implode('/', $inputUrlParts);
672
            } else {
673 3
                $outputUrlParts[] = $inputUrl;
674
            }
675 7
            $outputUrl = implode('/', $outputUrlParts);
676
677 7
            return str_replace($inputUrl, $outputUrl, $fullMatch);
678 16
        };
679
680 16
        $cssContent = preg_replace_callback('/url\(["\']?([^)^"\']*)["\']?\)/i', $callback, $cssContent);
681
682 16
        return $cssContent;
683
    }
684
685
    /**
686
     * Creates template of configuration file for [[actionCompress]].
687
     * @param string $configFile output file name.
688
     * @return int CLI exit code
689
     * @throws \yii\console\Exception on failure.
690
     */
691 1
    public function actionTemplate($configFile)
692
    {
693 1
        $jsCompressor = VarDumper::export($this->jsCompressor);
694 1
        $cssCompressor = VarDumper::export($this->cssCompressor);
695
696 1
        $template = <<<EOD
697 1
<?php
698
/**
699
 * Configuration file for the "yii asset" console command.
700
 */
701
702
// In the console environment, some path aliases may not exist. Please define these:
703
// Yii::setAlias('@webroot', __DIR__ . '/../web');
704
// Yii::setAlias('@web', '/');
705
706
return [
707
    // Adjust command/callback for JavaScript files compressing:
708 1
    'jsCompressor' => {$jsCompressor},
709
    // Adjust command/callback for CSS files compressing:
710 1
    'cssCompressor' => {$cssCompressor},
711
    // Whether to delete asset source after compression:
712
    'deleteSource' => false,
713
    // The list of asset bundles to compress:
714
    'bundles' => [
715
        // 'app\assets\AppAsset',
716
        // 'yii\web\YiiAsset',
717
        // 'yii\web\JqueryAsset',
718
    ],
719
    // Asset bundle for compression output:
720
    'targets' => [
721
        'all' => [
722
            'class' => 'yii\web\AssetBundle',
723
            'basePath' => '@webroot/assets',
724
            'baseUrl' => '@web/assets',
725
            'js' => 'js/all-{hash}.js',
726
            'css' => 'css/all-{hash}.css',
727
        ],
728
    ],
729
    // Asset manager configuration:
730
    'assetManager' => [
731
        //'basePath' => '@webroot/assets',
732
        //'baseUrl' => '@web/assets',
733
    ],
734
];
735 1
EOD;
736 1
        if (file_exists($configFile)) {
737
            if (!$this->confirm("File '{$configFile}' already exists. Do you wish to overwrite it?")) {
738
                return ExitCode::OK;
739
            }
740
        }
741 1
        if (!file_put_contents($configFile, $template, LOCK_EX)) {
742
            throw new Exception("Unable to write template file '{$configFile}'.");
743
        }
744
745 1
        $this->stdout("Configuration file template created at '{$configFile}'.\n\n", Console::FG_GREEN);
746 1
        return ExitCode::OK;
747
    }
748
749
    /**
750
     * Returns canonicalized absolute pathname.
751
     * Unlike regular `realpath()` this method does not expand symlinks and does not check path existence.
752
     * @param string $path raw path
753
     * @return string canonicalized absolute pathname
754
     */
755 9
    private function findRealPath($path)
756
    {
757 9
        $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
758 9
        $pathParts = explode(DIRECTORY_SEPARATOR, $path);
759
760 9
        $realPathParts = [];
761 9
        foreach ($pathParts as $pathPart) {
762 9
            if ($pathPart === '..') {
763 4
                array_pop($realPathParts);
764
            } else {
765 9
                $realPathParts[] = $pathPart;
766
            }
767
        }
768
769 9
        return implode(DIRECTORY_SEPARATOR, $realPathParts);
770
    }
771
772
    /**
773
     * @param AssetBundle $bundle
774
     * @return bool whether asset bundle external or not.
775
     */
776 4
    private function isBundleExternal($bundle)
777
    {
778 4
        return empty($bundle->sourcePath) && empty($bundle->basePath);
779
    }
780
781
    /**
782
     * @param AssetBundle $bundle asset bundle instance.
783
     * @return array bundle configuration.
784
     */
785 2
    private function composeBundleConfig($bundle)
786
    {
787 2
        $config = Yii::getObjectVars($bundle);
788 2
        $config['class'] = get_class($bundle);
789 2
        return $config;
790
    }
791
792
    /**
793
     * Composes trace info for bundle circular dependency.
794
     * @param string $circularDependencyName name of the bundle, which have circular dependency
795
     * @param array $registered list of bundles registered while detecting circular dependency.
796
     * @return string bundle circular dependency trace string.
797
     */
798 1
    private function composeCircularDependencyTrace($circularDependencyName, array $registered)
799
    {
800 1
        $dependencyTrace = [];
801 1
        $startFound = false;
802 1
        foreach ($registered as $name => $value) {
803 1
            if ($name === $circularDependencyName) {
804 1
                $startFound = true;
805
            }
806 1
            if ($startFound && $value === false) {
807 1
                $dependencyTrace[] = $name;
808
            }
809
        }
810 1
        $dependencyTrace[] = $circularDependencyName;
811 1
        return implode(' -> ', $dependencyTrace);
812
    }
813
814
    /**
815
     * Deletes bundle asset files, which have been published from `sourcePath`.
816
     * @param \yii\web\AssetBundle[] $bundles asset bundles to be processed.
817
     * @since 2.0.10
818
     */
819 1
    private function deletePublishedAssets($bundles)
820
    {
821 1
        $this->stdout("Deleting source files...\n");
822
823 1
        if ($this->getAssetManager()->linkAssets) {
824
            $this->stdout("`AssetManager::linkAssets` option is enabled. Deleting of source files canceled.\n", Console::FG_YELLOW);
825
            return;
826
        }
827
828 1
        foreach ($bundles as $bundle) {
829 1
            if ($bundle->sourcePath !== null) {
830 1
                foreach ($bundle->js as $jsFile) {
831 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 for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

831
                    /** @scrutinizer ignore-unhandled */ @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $jsFile);

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...
832
                }
833 1
                foreach ($bundle->css as $cssFile) {
834 1
                    @unlink($bundle->basePath . DIRECTORY_SEPARATOR . $cssFile);
835
                }
836
            }
837
        }
838
839 1
        $this->stdout("Source files deleted.\n", Console::FG_GREEN);
840
    }
841
}
842