Completed
Push — fix-6226-symlink-dir-exists-mu... ( 54ac18 )
by
unknown
11:35
created

AssetManager::publishFile()   C

Complexity

Conditions 8
Paths 14

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 0
cts 18
cp 0
rs 5.3846
c 0
b 0
f 0
cc 8
eloc 19
nc 14
nop 1
crap 72
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\web;
9
10
use Yii;
11
use yii\base\Component;
12
use yii\base\InvalidConfigException;
13
use yii\base\InvalidParamException;
14
use yii\helpers\FileHelper;
15
use yii\helpers\Url;
16
17
/**
18
 * AssetManager manages asset bundle configuration and loading.
19
 *
20
 * AssetManager is configured as an application component in [[\yii\web\Application]] by default.
21
 * You can access that instance via `Yii::$app->assetManager`.
22
 *
23
 * You can modify its configuration by adding an array to your application config under `components`
24
 * as shown in the following example:
25
 *
26
 * ```php
27
 * 'assetManager' => [
28
 *     'bundles' => [
29
 *         // you can override AssetBundle configs here
30
 *     ],
31
 * ]
32
 * ```
33
 *
34
 * For more details and usage information on AssetManager, see the [guide article on assets](guide:structure-assets).
35
 *
36
 * @property AssetConverterInterface $converter The asset converter. Note that the type of this property
37
 * differs in getter and setter. See [[getConverter()]] and [[setConverter()]] for details.
38
 *
39
 * @author Qiang Xue <[email protected]>
40
 * @since 2.0
41
 */
42
class AssetManager extends Component
43
{
44
    /**
45
     * @var array|bool list of asset bundle configurations. This property is provided to customize asset bundles.
46
     * When a bundle is being loaded by [[getBundle()]], if it has a corresponding configuration specified here,
47
     * the configuration will be applied to the bundle.
48
     *
49
     * The array keys are the asset bundle names, which typically are asset bundle class names without leading backslash.
50
     * The array values are the corresponding configurations. If a value is false, it means the corresponding asset
51
     * bundle is disabled and [[getBundle()]] should return null.
52
     *
53
     * If this property is false, it means the whole asset bundle feature is disabled and [[getBundle()]]
54
     * will always return null.
55
     *
56
     * The following example shows how to disable the bootstrap css file used by Bootstrap widgets
57
     * (because you want to use your own styles):
58
     *
59
     * ```php
60
     * [
61
     *     'yii\bootstrap\BootstrapAsset' => [
62
     *         'css' => [],
63
     *     ],
64
     * ]
65
     * ```
66
     */
67
    public $bundles = [];
68
    /**
69
     * @var string the root directory storing the published asset files.
70
     */
71
    public $basePath = '@webroot/assets';
72
    /**
73
     * @var string the base URL through which the published asset files can be accessed.
74
     */
75
    public $baseUrl = '@web/assets';
76
    /**
77
     * @var array mapping from source asset files (keys) to target asset files (values).
78
     *
79
     * This property is provided to support fixing incorrect asset file paths in some asset bundles.
80
     * When an asset bundle is registered with a view, each relative asset file in its [[AssetBundle::css|css]]
81
     * and [[AssetBundle::js|js]] arrays will be examined against this map. If any of the keys is found
82
     * to be the last part of an asset file (which is prefixed with [[AssetBundle::sourcePath]] if available),
83
     * the corresponding value will replace the asset and be registered with the view.
84
     * For example, an asset file `my/path/to/jquery.js` matches a key `jquery.js`.
85
     *
86
     * Note that the target asset files should be absolute URLs, domain relative URLs (starting from '/') or paths
87
     * relative to [[baseUrl]] and [[basePath]].
88
     *
89
     * In the following example, any assets ending with `jquery.min.js` will be replaced with `jquery/dist/jquery.js`
90
     * which is relative to [[baseUrl]] and [[basePath]].
91
     *
92
     * ```php
93
     * [
94
     *     'jquery.min.js' => 'jquery/dist/jquery.js',
95
     * ]
96
     * ```
97
     *
98
     * You may also use aliases while specifying map value, for example:
99
     *
100
     * ```php
101
     * [
102
     *     'jquery.min.js' => '@web/js/jquery/jquery.js',
103
     * ]
104
     * ```
105
     */
106
    public $assetMap = [];
107
    /**
108
     * @var bool whether to use symbolic link to publish asset files. Defaults to false, meaning
109
     * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published
110
     * assets will always be consistent with the source assets and there is no copy operation required.
111
     * This is especially useful during development.
112
     *
113
     * However, there are special requirements for hosting environments in order to use symbolic links.
114
     * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
115
     *
116
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible
117
     * to Web users. For example, for Apache Web server, the following configuration directive should be added
118
     * for the Web folder:
119
     *
120
     * ```apache
121
     * Options FollowSymLinks
122
     * ```
123
     */
124
    public $linkAssets = false;
125
    /**
126
     * @var int the permission to be set for newly published asset files.
127
     * This value will be used by PHP chmod() function. No umask will be applied.
128
     * If not set, the permission will be determined by the current environment.
129
     */
130
    public $fileMode;
131
    /**
132
     * @var int the permission to be set for newly generated asset directories.
133
     * This value will be used by PHP chmod() function. No umask will be applied.
134
     * Defaults to 0775, meaning the directory is read-writable by owner and group,
135
     * but read-only for other users.
136
     */
137
    public $dirMode = 0775;
138
    /**
139
     * @var callback a PHP callback that is called before copying each sub-directory or file.
140
     * This option is used only when publishing a directory. If the callback returns false, the copy
141
     * operation for the sub-directory or file will be cancelled.
142
     *
143
     * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
144
     * file to be copied from, while `$to` is the copy target.
145
     *
146
     * This is passed as a parameter `beforeCopy` to [[\yii\helpers\FileHelper::copyDirectory()]].
147
     */
148
    public $beforeCopy;
149
    /**
150
     * @var callback a PHP callback that is called after a sub-directory or file is successfully copied.
151
     * This option is used only when publishing a directory. The signature of the callback is the same as
152
     * for [[beforeCopy]].
153
     * This is passed as a parameter `afterCopy` to [[\yii\helpers\FileHelper::copyDirectory()]].
154
     */
155
    public $afterCopy;
156
    /**
157
     * @var bool whether the directory being published should be copied even if
158
     * it is found in the target directory. This option is used only when publishing a directory.
159
     * You may want to set this to be `true` during the development stage to make sure the published
160
     * directory is always up-to-date. Do not set this to true on production servers as it will
161
     * significantly degrade the performance.
162
     */
163
    public $forceCopy = false;
164
    /**
165
     * @var bool whether to append a timestamp to the URL of every published asset. When this is true,
166
     * the URL of a published asset may look like `/path/to/asset?v=timestamp`, where `timestamp` is the
167
     * last modification time of the published asset file.
168
     * You normally would want to set this property to true when you have enabled HTTP caching for assets,
169
     * because it allows you to bust caching when the assets are updated.
170
     * @since 2.0.3
171
     */
172
    public $appendTimestamp = false;
173
    /**
174
     * @var callable a callback that will be called to produce hash for asset directory generation.
175
     * The signature of the callback should be as follows:
176
     *
177
     * ```
178
     * function ($path)
179
     * ```
180
     *
181
     * where `$path` is the asset path. Note that the `$path` can be either directory where the asset
182
     * files reside or a single file. For a CSS file that uses relative path in `url()`, the hash
183
     * implementation should use the directory path of the file instead of the file path to include
184
     * the relative asset files in the copying.
185
     *
186
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash`
187
     * method.
188
     *
189
     * Example of an implementation using MD4 hash:
190
     *
191
     * ```php
192
     * function ($path) {
193
     *     return hash('md4', $path);
194
     * }
195
     * ```
196
     *
197
     * @since 2.0.6
198
     */
199
    public $hashCallback;
200
201
    private $_dummyBundles = [];
202
203
204
    /**
205
     * Initializes the component.
206
     * @throws InvalidConfigException if [[basePath]] is invalid
207
     */
208 83
    public function init()
209
    {
210 83
        parent::init();
211 83
        $this->basePath = Yii::getAlias($this->basePath);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Yii::getAlias($this->basePath) can also be of type boolean. However, the property $basePath is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
212 83
        if (!is_dir($this->basePath)) {
213
            throw new InvalidConfigException("The directory does not exist: {$this->basePath}");
214 83
        } elseif (!is_writable($this->basePath)) {
215
            throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}");
216
        }
217
218 83
        $this->basePath = realpath($this->basePath);
219 83
        $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/');
220 83
    }
221
222
    /**
223
     * Returns the named asset bundle.
224
     *
225
     * This method will first look for the bundle in [[bundles]]. If not found,
226
     * it will treat `$name` as the class of the asset bundle and create a new instance of it.
227
     *
228
     * @param string $name the class name of the asset bundle (without the leading backslash)
229
     * @param bool $publish whether to publish the asset files in the asset bundle before it is returned.
230
     * If you set this false, you must manually call `AssetBundle::publish()` to publish the asset files.
231
     * @return AssetBundle the asset bundle instance
232
     * @throws InvalidConfigException if $name does not refer to a valid asset bundle
233
     */
234 32
    public function getBundle($name, $publish = true)
235
    {
236 32
        if ($this->bundles === false) {
237
            return $this->loadDummyBundle($name);
238 32
        } elseif (!isset($this->bundles[$name])) {
239 26
            return $this->bundles[$name] = $this->loadBundle($name, [], $publish);
240 19
        } elseif ($this->bundles[$name] instanceof AssetBundle) {
241
            return $this->bundles[$name];
242 19
        } elseif (is_array($this->bundles[$name])) {
243 13
            return $this->bundles[$name] = $this->loadBundle($name, $this->bundles[$name], $publish);
244 6
        } elseif ($this->bundles[$name] === false) {
245 6
            return $this->loadDummyBundle($name);
246
        }
247
248
        throw new InvalidConfigException("Invalid asset bundle configuration: $name");
249
    }
250
251
    /**
252
     * Loads asset bundle class by name.
253
     *
254
     * @param string $name bundle name
255
     * @param array $config bundle object configuration
256
     * @param bool $publish if bundle should be published
257
     * @return AssetBundle
258
     * @throws InvalidConfigException if configuration isn't valid
259
     */
260 32
    protected function loadBundle($name, $config = [], $publish = true)
261
    {
262 32
        if (!isset($config['class'])) {
263 32
            $config['class'] = $name;
264
        }
265
        /* @var $bundle AssetBundle */
266 32
        $bundle = Yii::createObject($config);
267 32
        if ($publish) {
268 32
            $bundle->publish($this);
269
        }
270
271 32
        return $bundle;
272
    }
273
274
    /**
275
     * Loads dummy bundle by name.
276
     *
277
     * @param string $name
278
     * @return AssetBundle
279
     */
280 6
    protected function loadDummyBundle($name)
281
    {
282 6
        if (!isset($this->_dummyBundles[$name])) {
283 6
            $this->_dummyBundles[$name] = $this->loadBundle($name, [
284 6
                'sourcePath' => null,
285
                'js' => [],
286
                'css' => [],
287
                'depends' => [],
288
            ]);
289
        }
290
291 6
        return $this->_dummyBundles[$name];
292
    }
293
294
    /**
295
     * Returns the actual URL for the specified asset.
296
     * The actual URL is obtained by prepending either [[AssetBundle::$baseUrl]] or [[AssetManager::$baseUrl]] to the given asset path.
297
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
298
     * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]].
299
     * @return string the actual URL for the specified asset.
300
     */
301 10
    public function getAssetUrl($bundle, $asset)
302
    {
303 10
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
304
            if (strncmp($actualAsset, '@web/', 5) === 0) {
305
                $asset = substr($actualAsset, 5);
306
                $basePath = Yii::getAlias('@webroot');
307
                $baseUrl = Yii::getAlias('@web');
308
            } else {
309
                $asset = Yii::getAlias($actualAsset);
0 ignored issues
show
Bug introduced by
It seems like $actualAsset defined by $this->resolveAsset($bundle, $asset) on line 303 can also be of type boolean; however, yii\BaseYii::getAlias() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug Compatibility introduced by
The expression \Yii::getAlias($actualAsset); of type string|boolean adds the type boolean to the return on line 319 which is incompatible with the return type documented by yii\web\AssetManager::getAssetUrl of type string.
Loading history...
310
                $basePath = $this->basePath;
311
                $baseUrl = $this->baseUrl;
312
            }
313
        } else {
314 10
            $basePath = $bundle->basePath;
315 10
            $baseUrl = $bundle->baseUrl;
316
        }
317
318 10
        if (!Url::isRelative($asset) || strncmp($asset, '/', 1) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $asset defined by \Yii::getAlias($actualAsset) on line 309 can also be of type boolean; however, yii\helpers\BaseUrl::isRelative() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
319
            return $asset;
320
        }
321
322 10
        if ($this->appendTimestamp && ($timestamp = @filemtime("$basePath/$asset")) > 0) {
323
            return "$baseUrl/$asset?v=$timestamp";
324
        }
325
326 10
        return "$baseUrl/$asset";
327
    }
328
329
    /**
330
     * Returns the actual file path for the specified asset.
331
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
332
     * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]].
333
     * @return string|false the actual file path, or `false` if the asset is specified as an absolute URL
334
     */
335
    public function getAssetPath($bundle, $asset)
336
    {
337
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
338
            return Url::isRelative($actualAsset) ? $this->basePath . '/' . $actualAsset : false;
0 ignored issues
show
Bug introduced by
It seems like $actualAsset defined by $this->resolveAsset($bundle, $asset) on line 337 can also be of type boolean; however, yii\helpers\BaseUrl::isRelative() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
339
        }
340
341
        return Url::isRelative($asset) ? $bundle->basePath . '/' . $asset : false;
342
    }
343
344
    /**
345
     * @param AssetBundle $bundle
346
     * @param string $asset
347
     * @return string|bool
348
     */
349 10
    protected function resolveAsset($bundle, $asset)
350
    {
351 10
        if (isset($this->assetMap[$asset])) {
352
            return $this->assetMap[$asset];
353
        }
354 10
        if ($bundle->sourcePath !== null && Url::isRelative($asset)) {
355
            $asset = $bundle->sourcePath . '/' . $asset;
356
        }
357
358 10
        $n = mb_strlen($asset, Yii::$app->charset);
359 10
        foreach ($this->assetMap as $from => $to) {
360
            $n2 = mb_strlen($from, Yii::$app->charset);
361
            if ($n2 <= $n && substr_compare($asset, $from, $n - $n2, $n2) === 0) {
362
                return $to;
363
            }
364
        }
365
366 10
        return false;
367
    }
368
369
    private $_converter;
370
371
    /**
372
     * Returns the asset converter.
373
     * @return AssetConverterInterface the asset converter.
374
     */
375 28
    public function getConverter()
376
    {
377 28
        if ($this->_converter === null) {
378 28
            $this->_converter = Yii::createObject(AssetConverter::className());
379 21
        } elseif (is_array($this->_converter) || is_string($this->_converter)) {
380
            if (is_array($this->_converter) && !isset($this->_converter['class'])) {
381
                $this->_converter['class'] = AssetConverter::className();
382
            }
383
            $this->_converter = Yii::createObject($this->_converter);
384
        }
385
386 28
        return $this->_converter;
387
    }
388
389
    /**
390
     * Sets the asset converter.
391
     * @param array|AssetConverterInterface $value the asset converter. This can be either
392
     * an object implementing the [[AssetConverterInterface]], or a configuration
393
     * array that can be used to create the asset converter object.
394
     */
395
    public function setConverter($value)
396
    {
397
        $this->_converter = $value;
398
    }
399
400
    /**
401
     * @var array published assets
402
     */
403
    private $_published = [];
404
405
    /**
406
     * Publishes a file or a directory.
407
     *
408
     * This method will copy the specified file or directory to [[basePath]] so that
409
     * it can be accessed via the Web server.
410
     *
411
     * If the asset is a file, its file modification time will be checked to avoid
412
     * unnecessary file copying.
413
     *
414
     * If the asset is a directory, all files and subdirectories under it will be published recursively.
415
     * Note, in case $forceCopy is false the method only checks the existence of the target
416
     * directory to avoid repetitive copying (which is very expensive).
417
     *
418
     * By default, when publishing a directory, subdirectories and files whose name starts with a dot "."
419
     * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option
420
     * as explained in the `$options` parameter.
421
     *
422
     * Note: On rare scenario, a race condition can develop that will lead to a
423
     * one-time-manifestation of a non-critical problem in the creation of the directory
424
     * that holds the published assets. This problem can be avoided altogether by 'requesting'
425
     * in advance all the resources that are supposed to trigger a 'publish()' call, and doing
426
     * that in the application deployment phase, before system goes live. See more in the following
427
     * discussion: http://code.google.com/p/yii/issues/detail?id=2579
428
     *
429
     * @param string $path the asset (file or directory) to be published
430
     * @param array $options the options to be applied when publishing a directory.
431
     * The following options are supported:
432
     *
433
     * - only: array, list of patterns that the file paths should match if they want to be copied.
434
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
435
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
436
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
437
     *   This overrides [[beforeCopy]] if set.
438
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
439
     *   This overrides [[afterCopy]] if set.
440
     * - forceCopy: boolean, whether the directory being published should be copied even if
441
     *   it is found in the target directory. This option is used only when publishing a directory.
442
     *   This overrides [[forceCopy]] if set.
443
     *
444
     * @return array the path (directory or file path) and the URL that the asset is published as.
445
     * @throws InvalidParamException if the asset to be published does not exist.
446
     */
447 8
    public function publish($path, $options = [])
448
    {
449 8
        $path = Yii::getAlias($path);
450
451 8
        if (isset($this->_published[$path])) {
452 1
            return $this->_published[$path];
453
        }
454
455 8
        if (!is_string($path) || ($src = realpath($path)) === false) {
456
            throw new InvalidParamException("The file or directory to be published does not exist: $path");
457
        }
458
459 8
        if (is_file($src)) {
460
            return $this->_published[$path] = $this->publishFile($src);
461
        }
462
463 8
        return $this->_published[$path] = $this->publishDirectory($src, $options);
464
    }
465
466
    /**
467
     * Publishes a file.
468
     * @param string $src the asset file to be published
469
     * @return string[] the path and the URL that the asset is published as.
470
     * @throws InvalidParamException if the asset to be published does not exist.
471
     */
472
    protected function publishFile($src)
473
    {
474
        $dir = $this->hash($src);
475
        $fileName = basename($src);
476
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
477
        $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName;
478
479
        if (!is_dir($dstDir)) {
480
            FileHelper::createDirectory($dstDir, $this->dirMode, true);
481
        }
482
483
        if ($this->linkAssets) {
484
            if (!is_file($dstFile)) {
485
                try { // fix #6226 symlinking multi threaded
486
                    symlink($src, $dstFile);
487
                } catch (\Exception $e) {
488
                    if (!is_file($dstFile)) {
489
                        throw $e;
490
                    }
491
                }
492
            }
493
        } elseif (@filemtime($dstFile) < @filemtime($src)) {
494
            copy($src, $dstFile);
495
            if ($this->fileMode !== null) {
496
                @chmod($dstFile, $this->fileMode);
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...
497
            }
498
        }
499
500
        return [$dstFile, $this->baseUrl . "/$dir/$fileName"];
501
    }
502
503
    /**
504
     * Publishes a directory.
505
     * @param string $src the asset directory to be published
506
     * @param array $options the options to be applied when publishing a directory.
507
     * The following options are supported:
508
     *
509
     * - only: array, list of patterns that the file paths should match if they want to be copied.
510
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
511
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
512
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
513
     *   This overrides [[beforeCopy]] if set.
514
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
515
     *   This overrides [[afterCopy]] if set.
516
     * - forceCopy: boolean, whether the directory being published should be copied even if
517
     *   it is found in the target directory. This option is used only when publishing a directory.
518
     *   This overrides [[forceCopy]] if set.
519
     *
520
     * @return string[] the path directory and the URL that the asset is published as.
521
     * @throws InvalidParamException if the asset to be published does not exist.
522
     */
523 8
    protected function publishDirectory($src, $options)
524
    {
525 8
        $dir = $this->hash($src);
526 8
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
527 8
        if ($this->linkAssets) {
528 2
            if (!is_dir($dstDir)) {
529 2
                FileHelper::createDirectory(dirname($dstDir), $this->dirMode, true);
530
                try { // fix #6226 symlinking multi threaded
531 2
                    symlink($src, $dstDir);
532
                } catch (\Exception $e) {
533
                    if (!is_dir($dstDir)) {
534 2
                        throw $e;
535
                    }
536
                }
537
            }
538 6
        } elseif (!empty($options['forceCopy']) || ($this->forceCopy && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
539 6
            $opts = array_merge(
540 6
                $options,
541
                [
542 6
                    'dirMode' => $this->dirMode,
543 6
                    'fileMode' => $this->fileMode,
544
                    'copyEmptyDirectories' => false,
545
                ]
546
            );
547 6
            if (!isset($opts['beforeCopy'])) {
548 5
                if ($this->beforeCopy !== null) {
549 1
                    $opts['beforeCopy'] = $this->beforeCopy;
550
                } else {
551 4
                    $opts['beforeCopy'] = function ($from, $to) {
0 ignored issues
show
Unused Code introduced by
The parameter $to is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
552 4
                        return strncmp(basename($from), '.', 1) !== 0;
553
                    };
554
                }
555
            }
556 6
            if (!isset($opts['afterCopy']) && $this->afterCopy !== null) {
557
                $opts['afterCopy'] = $this->afterCopy;
558
            }
559 6
            FileHelper::copyDirectory($src, $dstDir, $opts);
560
        }
561
562 8
        return [$dstDir, $this->baseUrl . '/' . $dir];
563
    }
564
565
    /**
566
     * Returns the published path of a file path.
567
     * This method does not perform any publishing. It merely tells you
568
     * if the file or directory is published, where it will go.
569
     * @param string $path directory or file path being published
570
     * @return string|false string the published file path. False if the file or directory does not exist
571
     */
572
    public function getPublishedPath($path)
573
    {
574
        $path = Yii::getAlias($path);
575
576
        if (isset($this->_published[$path])) {
577
            return $this->_published[$path][0];
578
        }
579
        if (is_string($path) && ($path = realpath($path)) !== false) {
580
            return $this->basePath . DIRECTORY_SEPARATOR . $this->hash($path) . (is_file($path) ? DIRECTORY_SEPARATOR . basename($path) : '');
581
        }
582
583
        return false;
584
    }
585
586
    /**
587
     * Returns the URL of a published file path.
588
     * This method does not perform any publishing. It merely tells you
589
     * if the file path is published, what the URL will be to access it.
590
     * @param string $path directory or file path being published
591
     * @return string|false string the published URL for the file or directory. False if the file or directory does not exist.
592
     */
593
    public function getPublishedUrl($path)
594
    {
595
        $path = Yii::getAlias($path);
596
597
        if (isset($this->_published[$path])) {
598
            return $this->_published[$path][1];
599
        }
600
        if (is_string($path) && ($path = realpath($path)) !== false) {
601
            return $this->baseUrl . '/' . $this->hash($path) . (is_file($path) ? '/' . basename($path) : '');
602
        }
603
604
        return false;
605
    }
606
607
    /**
608
     * Generate a CRC32 hash for the directory path. Collisions are higher
609
     * than MD5 but generates a much smaller hash string.
610
     * @param string $path string to be hashed.
611
     * @return string hashed string.
612
     */
613 8
    protected function hash($path)
614
    {
615 8
        if (is_callable($this->hashCallback)) {
616 1
            return call_user_func($this->hashCallback, $path);
617
        }
618 7
        $path = (is_file($path) ? dirname($path) : $path) . filemtime($path);
619 7
        return sprintf('%x', crc32($path . Yii::getVersion()));
620
    }
621
}
622