Issues (1474)

framework/Web/TAssetManager.php (7 issues)

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
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