1: <?php
2: /**
3: * CAssetManager class file.
4: *
5: * @author Qiang Xue <qiang.xue@gmail.com>
6: * @link http://www.yiiframework.com/
7: * @copyright 2008-2013 Yii Software LLC
8: * @license http://www.yiiframework.com/license/
9: */
10:
11:
12: /**
13: * CAssetManager is a Web application component that manages private files (called assets) and makes them accessible by Web clients.
14: *
15: * It achieves this goal by copying assets to a Web-accessible directory
16: * and returns the corresponding URL for accessing them.
17: *
18: * To publish an asset, simply call {@link publish()}.
19: *
20: * The Web-accessible directory holding the published files is specified
21: * by {@link setBasePath basePath}, which defaults to the "assets" directory
22: * under the directory containing the application entry script file.
23: * The property {@link setBaseUrl baseUrl} refers to the URL for accessing
24: * the {@link setBasePath basePath}.
25: *
26: * @property string $basePath The root directory storing the published asset files. Defaults to 'WebRoot/assets'.
27: * @property string $baseUrl The base url that the published asset files can be accessed.
28: * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'.
29: *
30: * @author Qiang Xue <qiang.xue@gmail.com>
31: * @package system.web
32: * @since 1.0
33: */
34: class CAssetManager extends CApplicationComponent
35: {
36: /**
37: * Default web accessible base path for storing private files
38: */
39: const DEFAULT_BASEPATH='assets';
40: /**
41: * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning
42: * asset files are copied to public folders. Using symbolic links has the benefit that the published
43: * assets will always be consistent with the source assets. This is especially useful during development.
44: *
45: * However, there are special requirements for hosting environments in order to use symbolic links.
46: * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
47: * The latter requires PHP 5.3 or greater.
48: *
49: * Moreover, some Web servers need to be properly configured so that the linked assets are accessible
50: * to Web users. For example, for Apache Web server, the following configuration directive should be added
51: * for the Web folder:
52: * <pre>
53: * Options FollowSymLinks
54: * </pre>
55: *
56: * Note that this property cannot be true when {@link $forceCopy} property has true value too. Otherwise
57: * an exception would be thrown. Using both properties at the same time is illogical because both of them
58: * are solving similar tasks but in a different ways. Please refer to the {@link $forceCopy} documentation
59: * for more details.
60: *
61: * @since 1.1.5
62: */
63: public $linkAssets=false;
64: /**
65: * @var array list of directories and files which should be excluded from the publishing process.
66: * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled.
67: * @since 1.1.6
68: **/
69: public $excludeFiles=array('.svn','.gitignore');
70: /**
71: * @var integer the permission to be set for newly generated asset files.
72: * This value will be used by PHP chmod function.
73: * Defaults to 0666, meaning the file is read-writable by all users.
74: * @since 1.1.8
75: */
76: public $newFileMode=0666;
77: /**
78: * @var integer the permission to be set for newly generated asset directories.
79: * This value will be used by PHP chmod function.
80: * Defaults to 0777, meaning the directory can be read, written and executed by all users.
81: * @since 1.1.8
82: */
83: public $newDirMode=0777;
84: /**
85: * @var boolean whether we should copy the asset files and directories even if they already published before.
86: * This property is used only during development stage. The main use case of this property is when you need
87: * to force the original assets always copied by changing only one value without searching needed {@link publish}
88: * method calls across the application codebase. Also it is useful in operating systems which does not fully
89: * support symbolic links (therefore it is not possible to use {@link $linkAssets}) or we don't want to use them.
90: * This property sets the default value of the $forceCopy parameter in {@link publish} method. Default value
91: * of this property is false meaning that the assets will be published only in case they don't exist in webroot
92: * assets directory.
93: *
94: * Note that this property cannot be true when {@link $linkAssets} property has true value too. Otherwise
95: * an exception would be thrown. Using both properties at the same time is illogical because both of them
96: * are solving similar tasks but in a different ways. Please refer to the {@link $linkAssets} documentation
97: * for more details.
98: *
99: * @since 1.1.11
100: */
101: public $forceCopy=false;
102: /**
103: * @var string base web accessible path for storing private files
104: */
105: private $_basePath;
106: /**
107: * @var string base URL for accessing the publishing directory.
108: */
109: private $_baseUrl;
110: /**
111: * @var array published assets
112: */
113: private $_published=array();
114:
115: /**
116: * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'.
117: */
118: public function getBasePath()
119: {
120: if($this->_basePath===null)
121: {
122: $request=Yii::app()->getRequest();
123: $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH);
124: }
125: return $this->_basePath;
126: }
127:
128: /**
129: * Sets the root directory storing published asset files.
130: * @param string $value the root directory storing published asset files
131: * @throws CException if the base path is invalid
132: */
133: public function setBasePath($value)
134: {
135: if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath))
136: $this->_basePath=$basePath;
137: else
138: throw new CException(Yii::t('yii','CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.',
139: array('{path}'=>$value)));
140: }
141:
142: /**
143: * @return string the base url that the published asset files can be accessed.
144: * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'.
145: */
146: public function getBaseUrl()
147: {
148: if($this->_baseUrl===null)
149: {
150: $request=Yii::app()->getRequest();
151: $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH);
152: }
153: return $this->_baseUrl;
154: }
155:
156: /**
157: * @param string $value the base url that the published asset files can be accessed
158: */
159: public function setBaseUrl($value)
160: {
161: $this->_baseUrl=rtrim($value,'/');
162: }
163:
164: /**
165: * Publishes a file or a directory.
166: * This method will copy the specified asset to a web accessible directory
167: * and return the URL for accessing the published asset.
168: * <ul>
169: * <li>If the asset is a file, its file modification time will be checked
170: * to avoid unnecessary file copying;</li>
171: * <li>If the asset is a directory, all files and subdirectories under it will
172: * be published recursively. Note, in case $forceCopy is false the method only checks the
173: * existence of the target directory to avoid repetitive copying.</li>
174: * </ul>
175: *
176: * Note: On rare scenario, a race condition can develop that will lead to a
177: * one-time-manifestation of a non-critical problem in the creation of the directory
178: * that holds the published assets. This problem can be avoided altogether by 'requesting'
179: * in advance all the resources that are supposed to trigger a 'publish()' call, and doing
180: * that in the application deployment phase, before system goes live. See more in the following
181: * discussion: http://code.google.com/p/yii/issues/detail?id=2579
182: *
183: * @param string $path the asset (file or directory) to be published
184: * @param boolean $hashByName whether the published directory should be named as the hashed basename.
185: * If false, the name will be the hash taken from dirname of the path being published and path mtime.
186: * Defaults to false. Set true if the path being published is shared among
187: * different extensions.
188: * @param integer $level level of recursive copying when the asset is a directory.
189: * Level -1 means publishing all subdirectories and files;
190: * Level 0 means publishing only the files DIRECTLY under the directory;
191: * level N means copying those directories that are within N levels.
192: * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already
193: * published before. In case of publishing a directory old files will not be removed.
194: * This parameter is set true mainly during development stage when the original
195: * assets are being constantly changed. The consequence is that the performance is degraded,
196: * which is not a concern during development, however. Default value of this parameter is null meaning
197: * that it's value is controlled by {@link $forceCopy} class property. This parameter has been available
198: * since version 1.1.2. Default value has been changed since 1.1.11.
199: * Note that this parameter cannot be true when {@link $linkAssets} property has true value too. Otherwise
200: * an exception would be thrown. Using this parameter with {@link $linkAssets} property at the same time
201: * is illogical because both of them are solving similar tasks but in a different ways. Please refer
202: * to the {@link $linkAssets} documentation for more details.
203: * @return string an absolute URL to the published asset
204: * @throws CException if the asset to be published does not exist.
205: */
206: public function publish($path,$hashByName=false,$level=-1,$forceCopy=null)
207: {
208: if($forceCopy===null)
209: $forceCopy=$this->forceCopy;
210: if($forceCopy && $this->linkAssets)
211: throw new CException(Yii::t('yii','The "forceCopy" and "linkAssets" cannot be both true.'));
212: if(isset($this->_published[$path]))
213: return $this->_published[$path];
214: elseif(is_string($path) && ($src=realpath($path))!==false)
215: {
216: $dir=$this->generatePath($src,$hashByName);
217: $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir;
218: if(is_file($src))
219: {
220: $fileName=basename($src);
221: $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName;
222:
223: if(!is_dir($dstDir))
224: {
225: mkdir($dstDir,$this->newDirMode,true);
226: @chmod($dstDir,$this->newDirMode);
227: }
228:
229: if($this->linkAssets && !is_file($dstFile)) symlink($src,$dstFile);
230: elseif(@filemtime($dstFile)<@filemtime($src))
231: {
232: copy($src,$dstFile);
233: @chmod($dstFile,$this->newFileMode);
234: }
235:
236: return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName";
237: }
238: elseif(is_dir($src))
239: {
240: if($this->linkAssets && !is_dir($dstDir))
241: {
242: symlink($src,$dstDir);
243: }
244: elseif(!is_dir($dstDir) || $forceCopy)
245: {
246: CFileHelper::copyDirectory($src,$dstDir,array(
247: 'exclude'=>$this->excludeFiles,
248: 'level'=>$level,
249: 'newDirMode'=>$this->newDirMode,
250: 'newFileMode'=>$this->newFileMode,
251: ));
252: }
253:
254: return $this->_published[$path]=$this->getBaseUrl().'/'.$dir;
255: }
256: }
257: throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.',
258: array('{asset}'=>$path)));
259: }
260:
261: /**
262: * Returns the published path of a file path.
263: * This method does not perform any publishing. It merely tells you
264: * if the file or directory is published, where it will go.
265: * @param string $path directory or file path being published
266: * @param boolean $hashByName whether the published directory should be named as the hashed basename.
267: * If false, the name will be the hash taken from dirname of the path being published and path mtime.
268: * Defaults to false. Set true if the path being published is shared among
269: * different extensions.
270: * @return string the published file path. False if the file or directory does not exist
271: */
272: public function getPublishedPath($path,$hashByName=false)
273: {
274: if(is_string($path) && ($path=realpath($path))!==false)
275: {
276: $base=$this->getBasePath().DIRECTORY_SEPARATOR.$this->generatePath($path,$hashByName);
277: return is_file($path) ? $base.DIRECTORY_SEPARATOR.basename($path) : $base ;
278: }
279: else
280: return false;
281: }
282:
283: /**
284: * Returns the URL of a published file path.
285: * This method does not perform any publishing. It merely tells you
286: * if the file path is published, what the URL will be to access it.
287: * @param string $path directory or file path being published
288: * @param boolean $hashByName whether the published directory should be named as the hashed basename.
289: * If false, the name will be the hash taken from dirname of the path being published and path mtime.
290: * Defaults to false. Set true if the path being published is shared among
291: * different extensions.
292: * @return string the published URL for the file or directory. False if the file or directory does not exist.
293: */
294: public function getPublishedUrl($path,$hashByName=false)
295: {
296: if(isset($this->_published[$path]))
297: return $this->_published[$path];
298: if(is_string($path) && ($path=realpath($path))!==false)
299: {
300: $base=$this->getBaseUrl().'/'.$this->generatePath($path,$hashByName);
301: return is_file($path) ? $base.'/'.basename($path) : $base;
302: }
303: else
304: return false;
305: }
306:
307: /**
308: * Generate a CRC32 hash for the directory path. Collisions are higher
309: * than MD5 but generates a much smaller hash string.
310: * @param string $path string to be hashed.
311: * @return string hashed string.
312: */
313: protected function hash($path)
314: {
315: return sprintf('%x',crc32($path.Yii::getVersion()));
316: }
317:
318: /**
319: * Generates path segments relative to basePath.
320: * @param string $file for which public path will be created.
321: * @param bool $hashByName whether the published directory should be named as the hashed basename.
322: * @return string path segments without basePath.
323: * @since 1.1.13
324: */
325: protected function generatePath($file,$hashByName=false)
326: {
327: if (is_file($file))
328: $pathForHashing=$hashByName ? dirname($file) : dirname($file).filemtime($file);
329: else
330: $pathForHashing=$hashByName ? $file : $file.filemtime($file);
331:
332: return $this->hash($pathForHashing);
333: }
334: }
335: