pradosoft /
prado
| 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
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
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
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
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
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
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
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
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
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
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
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
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
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 |