AssetManager::hash()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

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

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

234
        $this->basePath = realpath(/** @scrutinizer ignore-type */ $this->basePath);
Loading history...
235 100
        $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/');
0 ignored issues
show
Bug introduced by
It seems like Yii::getAlias($this->baseUrl) can also be of type false; however, parameter $string of rtrim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

235
        $this->baseUrl = rtrim(/** @scrutinizer ignore-type */ Yii::getAlias($this->baseUrl), '/');
Loading history...
236
    }
237
238
    /**
239
     * @var bool|null
240
     */
241
    private $_isBasePathPermissionChecked;
242
243
    /**
244
     * Check whether the basePath exists and is writeable.
245
     *
246
     * @since 2.0.40
247
     */
248 14
    public function checkBasePathPermission()
249
    {
250
        // if the check is been done already, skip further checks
251 14
        if ($this->_isBasePathPermissionChecked) {
252
            return;
253
        }
254
255 14
        if (!is_dir($this->basePath)) {
256
            throw new InvalidConfigException("The directory does not exist: {$this->basePath}");
257
        }
258
259 14
        if (!is_writable($this->basePath)) {
260 1
            throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}");
261
        }
262
263 13
        $this->_isBasePathPermissionChecked = true;
264
    }
265
266
    /**
267
     * Returns the named asset bundle.
268
     *
269
     * This method will first look for the bundle in [[bundles]]. If not found,
270
     * it will treat `$name` as the class of the asset bundle and create a new instance of it.
271
     *
272
     * @param string $name the class name of the asset bundle (without the leading backslash)
273
     * @param bool $publish whether to publish the asset files in the asset bundle before it is returned.
274
     * If you set this false, you must manually call `AssetBundle::publish()` to publish the asset files.
275
     * @return AssetBundle the asset bundle instance
276
     * @throws InvalidConfigException if $name does not refer to a valid asset bundle
277
     */
278 39
    public function getBundle($name, $publish = true)
279
    {
280 39
        if ($this->bundles === false) {
281
            return $this->loadDummyBundle($name);
282 39
        } elseif (!isset($this->bundles[$name])) {
283 35
            return $this->bundles[$name] = $this->loadBundle($name, [], $publish);
284 25
        } elseif ($this->bundles[$name] instanceof AssetBundle) {
285 4
            return $this->bundles[$name];
286 21
        } elseif (is_array($this->bundles[$name])) {
287 13
            return $this->bundles[$name] = $this->loadBundle($name, $this->bundles[$name], $publish);
288 8
        } elseif ($this->bundles[$name] === false) {
289 8
            return $this->loadDummyBundle($name);
290
        }
291
292
        throw new InvalidConfigException("Invalid asset bundle configuration: $name");
293
    }
294
295
    /**
296
     * Loads asset bundle class by name.
297
     *
298
     * @param string $name bundle name
299
     * @param array $config bundle object configuration
300
     * @param bool $publish if bundle should be published
301
     * @return AssetBundle
302
     * @throws InvalidConfigException if configuration isn't valid
303
     */
304 36
    protected function loadBundle($name, $config = [], $publish = true)
305
    {
306 36
        if (!isset($config['class'])) {
307 36
            $config['class'] = $name;
308
        }
309
        /** @var AssetBundle $bundle */
310 36
        $bundle = Yii::createObject($config);
311 36
        if ($publish) {
312 36
            $bundle->publish($this);
313
        }
314
315 36
        return $bundle;
316
    }
317
318
    /**
319
     * Loads dummy bundle by name.
320
     *
321
     * @param string $name
322
     * @return AssetBundle
323
     */
324 8
    protected function loadDummyBundle($name)
325
    {
326 8
        if (!isset($this->_dummyBundles[$name])) {
327 8
            $bundle = Yii::createObject(['class' => $name]);
328 8
            $bundle->sourcePath = null;
329 8
            $bundle->js = [];
330 8
            $bundle->css = [];
331
332 8
            $this->_dummyBundles[$name] = $bundle;
333
        }
334
335 8
        return $this->_dummyBundles[$name];
336
    }
337
338
    /**
339
     * Returns the actual URL for the specified asset.
340
     * The actual URL is obtained by prepending either [[AssetBundle::$baseUrl]] or [[AssetManager::$baseUrl]] to the given asset path.
341
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
342
     * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]].
343
     * @param bool|null $appendTimestamp Whether to append timestamp to the URL.
344
     * @return string the actual URL for the specified asset.
345
     */
346 15
    public function getAssetUrl($bundle, $asset, $appendTimestamp = null)
347
    {
348 15
        $assetUrl = $this->getActualAssetUrl($bundle, $asset);
349 15
        $assetPath = $this->getAssetPath($bundle, $asset);
350
351 15
        $withTimestamp = $this->appendTimestamp;
352 15
        if ($appendTimestamp !== null) {
353 2
            $withTimestamp = $appendTimestamp;
354
        }
355
356 15
        if ($withTimestamp && $assetPath && ($timestamp = @filemtime($assetPath)) > 0) {
357 3
            return "$assetUrl?v=$timestamp";
358
        }
359
360 14
        return $assetUrl;
361
    }
362
363
    /**
364
     * Returns the actual file path for the specified asset.
365
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
366
     * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]].
367
     * @return string|false the actual file path, or `false` if the asset is specified as an absolute URL
368
     */
369 15
    public function getAssetPath($bundle, $asset)
370
    {
371 15
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
372
            return Url::isRelative($actualAsset) ? $this->basePath . '/' . $actualAsset : false;
373
        }
374
375 15
        return Url::isRelative($asset) ? $bundle->basePath . '/' . $asset : false;
376
    }
377
378
    /**
379
     * @param AssetBundle $bundle
380
     * @param string $asset
381
     * @return string|false
382
     */
383 15
    protected function resolveAsset($bundle, $asset)
384
    {
385 15
        if (isset($this->assetMap[$asset])) {
386
            return $this->assetMap[$asset];
387
        }
388 15
        if ($bundle->sourcePath !== null && Url::isRelative($asset)) {
389
            $asset = $bundle->sourcePath . '/' . $asset;
390
        }
391
392 15
        $n = mb_strlen($asset, Yii::$app->charset);
393 15
        foreach ($this->assetMap as $from => $to) {
394
            $n2 = mb_strlen($from, Yii::$app->charset);
395
            if ($n2 <= $n && substr_compare($asset, $from, $n - $n2, $n2) === 0) {
396
                return $to;
397
            }
398
        }
399
400 15
        return false;
401
    }
402
403
    /**
404
     * @var AssetConverterInterface
405
     */
406
    private $_converter;
407
408
    /**
409
     * Returns the asset converter.
410
     * @return AssetConverterInterface the asset converter.
411
     */
412 33
    public function getConverter()
413
    {
414 33
        if ($this->_converter === null) {
415 33
            $this->_converter = Yii::createObject(AssetConverter::class);
416 21
        } elseif (is_array($this->_converter) || is_string($this->_converter)) {
0 ignored issues
show
introduced by
The condition is_string($this->_converter) is always false.
Loading history...
417
            if (is_array($this->_converter) && !isset($this->_converter['class'])) {
418
                $this->_converter['class'] = AssetConverter::class;
419
            }
420
            $this->_converter = Yii::createObject($this->_converter);
421
        }
422
423 33
        return $this->_converter;
424
    }
425
426
    /**
427
     * Sets the asset converter.
428
     * @param array|AssetConverterInterface $value the asset converter. This can be either
429
     * an object implementing the [[AssetConverterInterface]], or a configuration
430
     * array that can be used to create the asset converter object.
431
     */
432
    public function setConverter($value)
433
    {
434
        $this->_converter = $value;
0 ignored issues
show
Documentation Bug introduced by
It seems like $value can also be of type array. However, the property $_converter is declared as type yii\web\AssetConverterInterface. 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...
435
    }
436
437
    /**
438
     * @var array published assets
439
     */
440
    private $_published = [];
441
442
    /**
443
     * Publishes a file or a directory.
444
     *
445
     * This method will copy the specified file or directory to [[basePath]] so that
446
     * it can be accessed via the Web server.
447
     *
448
     * If the asset is a file, its file modification time will be checked to avoid
449
     * unnecessary file copying.
450
     *
451
     * If the asset is a directory, all files and subdirectories under it will be published recursively.
452
     * Note, in case $forceCopy is false the method only checks the existence of the target
453
     * directory to avoid repetitive copying (which is very expensive).
454
     *
455
     * By default, when publishing a directory, subdirectories and files whose name starts with a dot "."
456
     * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option
457
     * as explained in the `$options` parameter.
458
     *
459
     * Note: On rare scenario, a race condition can develop that will lead to a
460
     * one-time-manifestation of a non-critical problem in the creation of the directory
461
     * that holds the published assets. This problem can be avoided altogether by 'requesting'
462
     * in advance all the resources that are supposed to trigger a 'publish()' call, and doing
463
     * that in the application deployment phase, before system goes live. See more in the following
464
     * discussion: https://code.google.com/archive/p/yii/issues/2579
465
     *
466
     * @param string $path the asset (file or directory) to be published
467
     * @param array $options the options to be applied when publishing a directory.
468
     * The following options are supported:
469
     *
470
     * - only: array, list of patterns that the file paths should match if they want to be copied.
471
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
472
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
473
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
474
     *   This overrides [[beforeCopy]] if set.
475
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
476
     *   This overrides [[afterCopy]] if set.
477
     * - forceCopy: boolean, whether the directory being published should be copied even if
478
     *   it is found in the target directory. This option is used only when publishing a directory.
479
     *   This overrides [[forceCopy]] if set.
480
     *
481
     * @return array the path (directory or file path) and the URL that the asset is published as.
482
     * @throws InvalidArgumentException if the asset to be published does not exist.
483
     * @throws InvalidConfigException if the target directory [[basePath]] is not writeable.
484
     *
485
     * @phpstan-param PublishOptions $options
486
     * @psalm-param PublishOptions $options
487
     */
488 14
    public function publish($path, $options = [])
489
    {
490 14
        $path = Yii::getAlias($path);
491
492 14
        if (isset($this->_published[$path])) {
493 1
            return $this->_published[$path];
494
        }
495
496 14
        if (!is_string($path) || ($src = realpath($path)) === false) {
497
            throw new InvalidArgumentException("The file or directory to be published does not exist: $path");
498
        }
499
500 14
        if (!is_readable($path)) {
501
            throw new InvalidArgumentException("The file or directory to be published is not readable: $path");
502
        }
503
504 14
        if (is_file($src)) {
505 1
            return $this->_published[$path] = $this->publishFile($src);
506
        }
507
508 13
        return $this->_published[$path] = $this->publishDirectory($src, $options);
509
    }
510
511
    /**
512
     * Publishes a file.
513
     * @param string $src the asset file to be published
514
     * @return string[] the path and the URL that the asset is published as.
515
     * @throws InvalidArgumentException if the asset to be published does not exist.
516
     */
517 1
    protected function publishFile($src)
518
    {
519 1
        $this->checkBasePathPermission();
520
521 1
        $dir = $this->hash($src);
522 1
        $fileName = basename($src);
523 1
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
524 1
        $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName;
525
526 1
        if (!is_dir($dstDir)) {
527 1
            FileHelper::createDirectory($dstDir, $this->dirMode, true);
528
        }
529
530 1
        if ($this->linkAssets) {
531
            if (!is_file($dstFile)) {
532
                try { // fix #6226 symlinking multi threaded
533
                    symlink($src, $dstFile);
534
                } catch (\Exception $e) {
535
                    if (!is_file($dstFile)) {
536
                        throw $e;
537
                    }
538
                }
539
            }
540 1
        } elseif (@filemtime($dstFile) < @filemtime($src)) {
541 1
            copy($src, $dstFile);
542 1
            if ($this->fileMode !== null) {
543
                @chmod($dstFile, $this->fileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). 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

543
                /** @scrutinizer ignore-unhandled */ @chmod($dstFile, $this->fileMode);

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...
544
            }
545
        }
546
547 1
        if ($this->appendTimestamp && ($timestamp = @filemtime($dstFile)) > 0) {
548 1
            $fileName = $fileName . "?v=$timestamp";
549
        }
550
551 1
        return [$dstFile, $this->baseUrl . "/$dir/$fileName"];
552
    }
553
554
    /**
555
     * Publishes a directory.
556
     * @param string $src the asset directory to be published
557
     * @param array $options the options to be applied when publishing a directory.
558
     * The following options are supported:
559
     *
560
     * - only: array, list of patterns that the file paths should match if they want to be copied.
561
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from being copied.
562
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to true.
563
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
564
     *   This overrides [[beforeCopy]] if set.
565
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
566
     *   This overrides [[afterCopy]] if set.
567
     * - forceCopy: boolean, whether the directory being published should be copied even if
568
     *   it is found in the target directory. This option is used only when publishing a directory.
569
     *   This overrides [[forceCopy]] if set.
570
     *
571
     * @return string[] the path directory and the URL that the asset is published as.
572
     * @throws InvalidArgumentException if the asset to be published does not exist.
573
     */
574 13
    protected function publishDirectory($src, $options)
575
    {
576 13
        $this->checkBasePathPermission();
577
578 12
        $dir = $this->hash($src);
579 12
        $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir;
580 12
        if ($this->linkAssets) {
581 2
            if (!is_dir($dstDir)) {
582 2
                FileHelper::createDirectory(dirname($dstDir), $this->dirMode, true);
583
                try { // fix #6226 symlinking multi threaded
584 2
                    symlink($src, $dstDir);
585
                } catch (\Exception $e) {
586
                    if (!is_dir($dstDir)) {
587 2
                        throw $e;
588
                    }
589
                }
590
            }
591 10
        } elseif (!empty($options['forceCopy']) || ($this->forceCopy && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
592 6
            $opts = array_merge(
593 6
                $options,
594 6
                [
595 6
                    'dirMode' => $this->dirMode,
596 6
                    'fileMode' => $this->fileMode,
597 6
                    'copyEmptyDirectories' => false,
598 6
                ]
599 6
            );
600 6
            if (!isset($opts['beforeCopy'])) {
601 5
                if ($this->beforeCopy !== null) {
602 1
                    $opts['beforeCopy'] = $this->beforeCopy;
603
                } else {
604 4
                    $opts['beforeCopy'] = function ($from, $to) {
0 ignored issues
show
Unused Code introduced by
The parameter $to is not used and could be removed. ( Ignorable by Annotation )

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

604
                    $opts['beforeCopy'] = function ($from, /** @scrutinizer ignore-unused */ $to) {

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

Loading history...
605 4
                        return strncmp(basename($from), '.', 1) !== 0;
606 4
                    };
607
                }
608
            }
609 6
            if (!isset($opts['afterCopy']) && $this->afterCopy !== null) {
610
                $opts['afterCopy'] = $this->afterCopy;
611
            }
612 6
            FileHelper::copyDirectory($src, $dstDir, $opts);
613
        }
614
615 12
        return [$dstDir, $this->baseUrl . '/' . $dir];
616
    }
617
618
    /**
619
     * Returns the published path of a file path.
620
     * This method does not perform any publishing. It merely tells you
621
     * if the file or directory is published, where it will go.
622
     * @param string $path directory or file path being published
623
     * @return string|false string the published file path. False if the file or directory does not exist
624
     */
625
    public function getPublishedPath($path)
626
    {
627
        $path = Yii::getAlias($path);
628
629
        if (isset($this->_published[$path])) {
630
            return $this->_published[$path][0];
631
        }
632
        if (is_string($path) && ($path = realpath($path)) !== false) {
633
            return $this->basePath . DIRECTORY_SEPARATOR . $this->hash($path) . (is_file($path) ? DIRECTORY_SEPARATOR . basename($path) : '');
634
        }
635
636
        return false;
637
    }
638
639
    /**
640
     * Returns the URL of a published file path.
641
     * This method does not perform any publishing. It merely tells you
642
     * if the file path is published, what the URL will be to access it.
643
     * @param string $path directory or file path being published
644
     * @return string|false string the published URL for the file or directory. False if the file or directory does not exist.
645
     */
646
    public function getPublishedUrl($path)
647
    {
648
        $path = Yii::getAlias($path);
649
650
        if (isset($this->_published[$path])) {
651
            return $this->_published[$path][1];
652
        }
653
        if (is_string($path) && ($path = realpath($path)) !== false) {
654
            return $this->baseUrl . '/' . $this->hash($path) . (is_file($path) ? '/' . basename($path) : '');
655
        }
656
657
        return false;
658
    }
659
660
    /**
661
     * Generate a CRC32 hash for the directory path. Collisions are higher
662
     * than MD5 but generates a much smaller hash string.
663
     * @param string $path string to be hashed.
664
     * @return string hashed string.
665
     */
666 13
    protected function hash($path)
667
    {
668 13
        if (is_callable($this->hashCallback)) {
669 1
            return call_user_func($this->hashCallback, $path);
0 ignored issues
show
Bug introduced by
It seems like $this->hashCallback can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

669
            return call_user_func(/** @scrutinizer ignore-type */ $this->hashCallback, $path);
Loading history...
670
        }
671 12
        $path = (is_file($path) ? dirname($path) : $path) . filemtime($path);
672 12
        return sprintf('%x', crc32($path . Yii::getVersion() . '|' . $this->linkAssets));
673
    }
674
675
    /**
676
     * Returns the actual URL for the specified asset. Without parameters.
677
     * The actual URL is obtained by prepending either [[AssetBundle::$baseUrl]] or [[AssetManager::$baseUrl]] to the given asset path.
678
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
679
     * @param string $asset the asset path. This should be one of the assets listed in [[AssetBundle::$js]] or [[AssetBundle::$css]].
680
     * @return string the actual URL for the specified asset.
681
     * @since 2.0.39
682
     */
683 15
    public function getActualAssetUrl($bundle, $asset)
684
    {
685 15
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
686
            if (strncmp($actualAsset, '@web/', 5) === 0) {
687
                $asset = substr($actualAsset, 5);
688
                $baseUrl = Yii::getAlias('@web');
689
            } else {
690
                $asset = Yii::getAlias($actualAsset);
691
                $baseUrl = $this->baseUrl;
692
            }
693
        } else {
694 15
            $baseUrl = $bundle->baseUrl;
695
        }
696
697 15
        if (!Url::isRelative($asset) || strncmp($asset, '/', 1) === 0) {
0 ignored issues
show
Bug introduced by
It seems like $asset can also be of type false; however, parameter $url of yii\helpers\BaseUrl::isRelative() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

697
        if (!Url::isRelative(/** @scrutinizer ignore-type */ $asset) || strncmp($asset, '/', 1) === 0) {
Loading history...
Bug introduced by
It seems like $asset can also be of type false; however, parameter $string1 of strncmp() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

697
        if (!Url::isRelative($asset) || strncmp(/** @scrutinizer ignore-type */ $asset, '/', 1) === 0) {
Loading history...
698 2
            return $asset;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $asset could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
699
        }
700
701 15
        return "$baseUrl/$asset";
702
    }
703
}
704