TAssetManager::publishFilePath()   B
last analyzed

Complexity

Conditions 11
Paths 6

Size

Total Lines 21
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 11.044

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 17
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 21
ccs 13
cts 14
cp 0.9286
crap 11.044
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TAssetManager class
5
 *
6
 * @author Qiang Xue <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Web;
12
13
use Prado\Exceptions\TConfigurationException;
14
use Prado\Exceptions\TInvalidDataValueException;
15
use Prado\Exceptions\TInvalidOperationException;
16
use Prado\Exceptions\TIOException;
17
use Prado\Prado;
18
use Prado\TApplicationMode;
19
use Prado\IO\TTarFileExtractor;
20
21
/**
22
 * TAssetManager class
23
 *
24
 * TAssetManager provides a scheme to allow web clients visiting
25
 * private files that are normally web-inaccessible.
26
 *
27
 * TAssetManager will copy the file to be published into a web-accessible
28
 * directory. The default base directory for storing the file is "assets", which
29
 * should be under the application directory. This can be changed by setting
30
 * the {@see setBasePath BasePath} property together with the
31
 * {@see setBaseUrl BaseUrl} property that refers to the URL for accessing the base path.
32
 *
33
 * By default, TAssetManager will not publish a file or directory if it already
34
 * exists in the publishing directory and has an older modification time.
35
 * If the application mode is set as 'Performance', the modification time check
36
 * will be skipped. You can explicitly require a modification time check
37
 * with the function {@see publishFilePath}. This is usually
38
 * very useful during development.
39
 *
40
 * TAssetManager may be configured in application configuration file as follows,
41
 * ```xml
42
 * <module id="asset" BasePath="Application.assets" BaseUrl="/assets" />
43
 * ```
44
 * where {@see getBasePath BasePath} and {@see getBaseUrl BaseUrl} are
45
 * configurable properties of TAssetManager. Make sure that BasePath is a namespace
46
 * pointing to a valid directory writable by the Web server process.
47
 *
48
 * @author Qiang Xue <[email protected]>
49
 * @since 3.0
50
 */
51
class TAssetManager extends \Prado\TModule
52
{
53
	/**
54
	 * Default web accessible base path for storing private files
55
	 */
56
	public const DEFAULT_BASEPATH = 'assets';
57
	/**
58
	 * @var string base web accessible path for storing private files
59
	 */
60
	private $_basePath;
61
	/**
62
	 * @var string base URL for accessing the publishing directory.
63
	 */
64
	private $_baseUrl;
65
	/**
66
	 * @var bool whether to use timestamp checking to ensure files are published with up-to-date versions.
67
	 */
68
	private $_checkTimestamp = false;
0 ignored issues
show
introduced by
The private property $_checkTimestamp is not used, and could be removed.
Loading history...
69
	/**
70
	 * @var array published assets
71
	 */
72
	private $_published = [];
73
	/**
74
	 * @var bool whether the module is initialized
75
	 */
76
	private $_initialized = false;
77
78
	/**
79
	 * Initializes the module.
80
	 * This method is required by IModule and is invoked by application.
81
	 * @param \Prado\Xml\TXmlElement $config module configuration
82
	 */
83
	public function init($config)
84
	{
85
		$application = $this->getApplication();
86
		if ($this->_basePath === null) {
87
			$this->_basePath = dirname($application->getRequest()->getApplicationFilePath()) . DIRECTORY_SEPARATOR . self::DEFAULT_BASEPATH;
88 6
		}
89
		if (!is_writable($this->_basePath) || !is_dir($this->_basePath)) {
90 6
			throw new TConfigurationException('assetmanager_basepath_invalid', $this->_basePath);
91 6
		}
92 5
		if ($this->_baseUrl === null) {
93
			$this->_baseUrl = rtrim(dirname($application->getRequest()->getApplicationUrl()), '/\\') . '/' . self::DEFAULT_BASEPATH;
94 6
		}
95 1
		$application->setAssetManager($this);
96
		$this->_initialized = true;
97 6
		parent::init($config);
98 2
	}
99
100 6
	/**
101 6
	 * @return string the root directory storing published asset files
102 6
	 */
103
	public function getBasePath()
104
	{
105
		return $this->_basePath;
106
	}
107 2
108
	/**
109 2
	 * Sets the root directory storing published asset files.
110
	 * The directory must be in namespace format.
111
	 * @param string $value the root directory storing published asset files
112
	 * @throws TInvalidOperationException if the module is initialized already
113
	 */
114
	public function setBasePath($value)
115
	{
116
		if ($this->_initialized) {
117
			throw new TInvalidOperationException('assetmanager_basepath_unchangeable');
118 1
		} else {
119
			$this->_basePath = Prado::getPathOfNamespace($value);
120 1
			if ($this->_basePath === null || !is_dir($this->_basePath) || !is_writable($this->_basePath)) {
121 1
				throw new TInvalidDataValueException('assetmanager_basepath_invalid', $value);
122
			}
123 1
		}
124 1
	}
125 1
126
	/**
127
	 * @return string the base url that the published asset files can be accessed
128 1
	 */
129
	public function getBaseUrl()
130
	{
131
		return $this->_baseUrl;
132
	}
133 1
134
	/**
135 1
	 * @param string $value the base url that the published asset files can be accessed
136
	 * @throws TInvalidOperationException if the module is initialized already
137
	 */
138
	public function setBaseUrl($value)
139
	{
140
		if ($this->_initialized) {
141
			throw new TInvalidOperationException('assetmanager_baseurl_unchangeable');
142 4
		} else {
143
			$this->_baseUrl = rtrim($value, '/');
144 4
		}
145 1
	}
146
147 4
	/**
148
	 * Publishes a file or a directory (recursively).
149 4
	 * This method will copy the content in a directory (recursively) to
150
	 * a web accessible directory and returns the URL for the directory.
151
	 * If the application is not in performance mode, the file modification
152
	 * time will be used to make sure the published file is latest or not.
153
	 * If not, a file copy will be performed.
154
	 * @param string $path the path to be published
155
	 * @param bool $checkTimestamp If true, file modification time will be checked even if the application
156
	 * is in performance mode.
157
	 * @throws TInvalidDataValueException if the file path to be published is
158
	 * invalid
159
	 * @return string an absolute URL to the published directory
160
	 */
161
	public function publishFilePath($path, $checkTimestamp = false)
162
	{
163
		if (isset($this->_published[$path])) {
164
			return $this->_published[$path];
165 2
		} elseif (empty($path) || ($fullpath = realpath($path)) === false) {
166
			throw new TInvalidDataValueException('assetmanager_filepath_invalid', $path);
167 2
		} elseif (is_file($fullpath)) {
168
			$dir = $this->hash(dirname($fullpath));
169 2
			$fileName = basename($fullpath);
170 1
			$dst = $this->_basePath . DIRECTORY_SEPARATOR . $dir;
171 2
			if (!is_file($dst . DIRECTORY_SEPARATOR . $fileName) || $checkTimestamp || $this->getApplication()->getMode() !== TApplicationMode::Performance) {
172 1
				$this->copyFile($fullpath, $dst);
173 1
			}
174 1
			return $this->_published[$path] = $this->_baseUrl . '/' . $dir . '/' . $fileName;
175 1
		} else {
176 1
			$dir = $this->hash($fullpath);
177
			if (!is_dir($this->_basePath . DIRECTORY_SEPARATOR . $dir) || $checkTimestamp || $this->getApplication()->getMode() !== TApplicationMode::Performance) {
178 1
				Prado::trace("Publishing directory $fullpath", TAssetManager::class);
179
				$this->copyDirectory($fullpath, $this->_basePath . DIRECTORY_SEPARATOR . $dir);
180 1
			}
181 1
			return $this->_published[$path] = $this->_baseUrl . '/' . $dir;
182 1
		}
183 1
	}
184
185 1
	/**
186
	 * @return array List of published assets
187
	 * @since 3.1.6
188
	 */
189
	public function getPublished()
190
	{
191
		return $this->_published;
192
	}
193
194
	/**
195
	 * @param array $values List of published assets
196
	 * @since 3.1.6
197
	 */
198
	protected function setPublished($values = [])
199
	{
200
		$this->_published = $values;
201
	}
202
203
	/**
204
	 * Returns the published path of a file path.
205
	 * This method does not perform any publishing. It merely tells you
206
	 * if the file path is published, where it will go.
207
	 * @param string $path directory or file path being published
208
	 * @return string the published file path
209
	 */
210
	public function getPublishedPath($path)
211
	{
212
		$path = realpath($path);
213
		if (is_file($path)) {
214 2
			return $this->_basePath . DIRECTORY_SEPARATOR . $this->hash(dirname($path)) . DIRECTORY_SEPARATOR . basename($path);
215
		} else {
216 2
			return $this->_basePath . DIRECTORY_SEPARATOR . $this->hash($path);
217 2
		}
218 1
	}
219
220 1
	/**
221
	 * Returns the URL of a published file path.
222
	 * This method does not perform any publishing. It merely tells you
223
	 * if the file path is published, what the URL will be to access it.
224
	 * @param string $path directory or file path being published
225
	 * @return string the published URL for the file path
226
	 */
227
	public function getPublishedUrl($path)
228
	{
229
		$path = realpath($path);
230
		if (is_file($path)) {
231 2
			return $this->_baseUrl . '/' . $this->hash(dirname($path)) . '/' . basename($path);
232
		} else {
233 2
			return $this->_baseUrl . '/' . $this->hash($path);
234 2
		}
235 1
	}
236
237 1
	/**
238
	 * Generate a CRC32 hash for the directory path. Collisions are higher
239
	 * than MD5 but generates a much smaller hash string.
240
	 * @param string $dir string to be hashed.
241
	 * @return string hashed string.
242
	 */
243
	protected function hash($dir)
244
	{
245
		return sprintf('%x', crc32($dir . Prado::getVersion()));
246
	}
247 3
248
	/**
249 3
	 * Copies a file to a directory.
250
	 * Copying is done only when the destination file does not exist
251
	 * or has an older file modification time.
252
	 * @param string $src source file path
253
	 * @param string $dst destination directory (if not exists, it will be created)
254
	 */
255
	protected function copyFile($src, $dst)
256
	{
257
		if (!is_dir($dst)) {
258
			@mkdir($dst);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). 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

258
			/** @scrutinizer ignore-unhandled */ @mkdir($dst);

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...
259 2
			@chmod($dst, Prado::getDefaultDirPermissions());
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

259
			/** @scrutinizer ignore-unhandled */ @chmod($dst, Prado::getDefaultDirPermissions());

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...
260
		}
261 2
		$dstFile = $dst . DIRECTORY_SEPARATOR . basename($src);
262 2
		if (@filemtime($dstFile) < @filemtime($src)) {
263 2
			Prado::trace("Publishing file $src to $dstFile", TAssetManager::class);
264
			@copy($src, $dstFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). 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

264
			/** @scrutinizer ignore-unhandled */ @copy($src, $dstFile);

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...
265 2
		}
266 2
	}
267 2
268 2
	/**
269
	 * Copies a directory recursively as another.
270 2
	 * If the destination directory does not exist, it will be created.
271
	 * File modification time is used to ensure the copied files are latest.
272
	 * @param string $src the source directory
273
	 * @param string $dst the destination directory
274
	 * @todo a generic solution to ignore certain directories and files
275
	 */
276
	public function copyDirectory($src, $dst)
277
	{
278
		if (!is_dir($dst)) {
279
			@mkdir($dst);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). 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

279
			/** @scrutinizer ignore-unhandled */ @mkdir($dst);

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...
280 1
			@chmod($dst, Prado::getDefaultDirPermissions());
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

280
			/** @scrutinizer ignore-unhandled */ @chmod($dst, Prado::getDefaultDirPermissions());

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...
281
		}
282 1
		if ($folder = @opendir($src)) {
283 1
			while ($file = @readdir($folder)) {
284 1
				if ($file === '.' || $file === '..' || $file === '.svn' || $file === '.git') {
285
					continue;
286 1
				} elseif (is_file($src . DIRECTORY_SEPARATOR . $file)) {
287 1
					if (@filemtime($dst . DIRECTORY_SEPARATOR . $file) < @filemtime($src . DIRECTORY_SEPARATOR . $file)) {
288 1
						@copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for copy(). 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

288
						/** @scrutinizer ignore-unhandled */ @copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);

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...
289 1
						@chmod($dst . DIRECTORY_SEPARATOR . $file, Prado::getDefaultFilePermissions());
290 1
					}
291 1
				} else {
292 1
					$this->copyDirectory($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
293 1
				}
294
			}
295
			closedir($folder);
296 1
		} else {
297
			throw new TInvalidDataValueException('assetmanager_source_directory_invalid', $src);
298
		}
299 1
	}
300
301
	/**
302
	 * Publish a tar file by extracting its contents to the assets directory.
303 1
	 * Each tar file must be accomplished with its own MD5 check sum file.
304
	 * The MD5 file is published when the tar contents are successfully
305
	 * extracted to the assets directory. The presence of the MD5 file
306
	 * as published asset assumes that the tar file has already been extracted.
307
	 * @param string $tarfile tar filename
308
	 * @param string $md5sum MD5 checksum for the corresponding tar file.
309
	 * @param bool $checkTimestamp Wether or not to check the time stamp of the file for publishing. Defaults to false.
310
	 * @return string URL path to the directory where the tar file was extracted.
311
	 */
312
	public function publishTarFile($tarfile, $md5sum, $checkTimestamp = false)
313
	{
314
		if (isset($this->_published[$md5sum])) {
315
			return $this->_published[$md5sum];
316 1
		} elseif (($fullpath = realpath($md5sum)) === false || !is_file($fullpath)) {
317
			throw new TInvalidDataValueException('assetmanager_tarchecksum_invalid', $md5sum);
318 1
		} else {
319
			$dir = $this->hash(dirname($fullpath));
320 1
			$fileName = basename($fullpath);
321 1
			$dst = $this->_basePath . DIRECTORY_SEPARATOR . $dir;
322
			if (!is_file($dst . DIRECTORY_SEPARATOR . $fileName) || $checkTimestamp || $this->getApplication()->getMode() !== TApplicationMode::Performance) {
323 1
				if (@filemtime($dst . DIRECTORY_SEPARATOR . $fileName) < @filemtime($fullpath)) {
324 1
					$this->copyFile($fullpath, $dst);
325 1
					$this->deployTarFile($tarfile, $dst);
326 1
				}
327 1
			}
328 1
			return $this->_published[$md5sum] = $this->_baseUrl . '/' . $dir;
329 1
		}
330
	}
331
332 1
	/**
333
	 * Extracts the tar file to the destination directory.
334
	 * N.B Tar file must not be compressed.
335
	 * @param string $path tar file
336
	 * @param string $destination path where the contents of tar file are to be extracted
337
	 * @return bool true if extract successful, false otherwise.
338
	 */
339
	protected function deployTarFile($path, $destination)
340
	{
341
		if (($fullpath = realpath($path)) === false || !is_file($fullpath)) {
342
			throw new TIOException('assetmanager_tarfile_invalid', $path);
343 1
		} else {
344
			$tar = new TTarFileExtractor($fullpath);
345 1
			return $tar->extract($destination);
346
		}
347
	}
348
}
349