Gentics Portal.Node PHP API
 All Classes Namespaces Functions Variables Pages
ExtendedClientScript.php
1 <?php
2 /**
3  * Compress and cache used JS and CSS files.
4  * Needs jsmin and cssmin
5  *
6  * Ties into the 1.0.4 and up Yii CClientScript functions
7  *
8  * 0.9.0 Now using CssMin code.google.com/p/cssmin/ for PHP 5.3.x compatibility
9  *
10  * Now checking and excluding remote files automatically
11  *
12  * @author Maxximus <maxximus007@gmail.com>
13  * @author Alexander Makarov <sam@rmcreative.ru>
14  * @author Kir <>
15  *
16  * @link http://www.yiiframework.com/
17  * @copyright Copyright &copy; 2008-2011
18  * @license htp://www.yiiframework.com/license/
19  * @version 0.9.0
20  *
21  */
22 
23 class ExtendedClientScript extends CClientScript
24 {
25 
26  public $disableRegisterFiles = false;
27  /**
28  * Compress all Javascript files with JSMin. JSMin must be installed as an extension in $jssminPath.
29  * github.com/rgrove/jsmin-php/
30  */
31  public $compressCombinedJs = false;
32  /**
33  * Compress all CSS files with CssMin. CssMin must be installed as an extension in $cssMinPath.
34  * Specific browserhacks will be removed, so don't add them in to be compressed CSS files.
35  * code.google.com/p/cssmin/
36  */
37  public $compressCombinedCss = false;
38  /**
39  * DEPRECATED/LEGACY
40  * Combine all JS and CSS files into one. Be careful with relative paths in CSS.
41  */
42  public $combineFiles = false;
43  /**
44  * Combine all non-remote JS files into one.
45  */
46  public $combineJs = false;
47  /**
48  * Combine all non-remote CSS files into one. Be careful with relative paths in CSS.
49  */
50  public $combineCss = false;
51  /**
52  * Exclude certain files from inclusion. array('/path/to/excluded/file') Useful for fixed base
53  * and incidental additional JS.
54  */
55  public $excludeFiles = array();
56  /**
57  * Path where the combined/compressed file will be stored. Will use coreScriptUrl if not defined
58  */
59  public $filePath;
60 
61  /**
62  * @var string domain for splitting js/css files across domains
63  */
64  public $splitDomainName;
65 
66  /**
67  * Relative Url where the combined/compressed file can be found
68  */
69  public $fileUrl;
70  /**
71  * Path where files can be found
72  */
73  public $basePath;
74  /**
75  * Used for garbage collection. If not accessed during that period: remove.
76  */
77  public $ttlDays = 1;
78  /**
79  * prefix for the combined/compressed files
80  */
81  public $prefix = 'c_';
82  /**
83  * path to JsMin
84  */
85  public $jsMinPath = 'ext.ExtendedClientScript.jsmin.*';
86  /**
87  * path to CssMin
88  */
89  public $cssMinPath = 'ext.ExtendedClientScript.cssmin.*';
90  /**
91  * CssMin filter options. Default values according cssMin doc.
92  */
93  public $cssMinFilters = array
94  (
95  'ImportImports' => false,
96  'RemoveComments' => true,
97  'RemoveEmptyRulesets' => true,
98  'RemoveEmptyAtBlocks' => true,
99  'ConvertLevel3AtKeyframes' => false,
100  'ConvertLevel3Properties' => false,
101  'Variables' => true,
102  'RemoveLastDelarationSemiColon' => true
103  );
104  /**
105  * CssMin plugin options. Maximum compression and conversion.
106  */
107  public $cssMinPlugins = array
108  (
109  'Variables' => true,
110  'ConvertFontWeight' => true,
111  'ConvertHslColors' => true,
112  'ConvertRgbColors' => true,
113  'ConvertNamedColors' => true,
114  'CompressColorValues' => true,
115  'CompressUnitValues' => true,
116  'CompressExpressionValues' => true,
117  );
118 
119  private $_timestamps = array();
120  private $_gccFile = array();
121 
122  /**
123  * Registers a script package that is listed in {@link packages}.
124  * @param string $name the name of the script package.
125  * @return CClientScript the CClientScript object itself (to support method chaining, available since version 1.1.5).
126  * @see renderCoreScript
127  */
128  public function registerCoreScript($name, $force = false)
129  {
130  if ($this->disableRegisterFiles && !$force) {
131  return $this;
132  }
133  return parent::registerCoreScript($name);
134  }
135 
136  public function registerCssFile($file, $media = '', $timestamp = null, $combine = true, $gccFile = false, $force = false)
137  {
138  if ($this->disableRegisterFiles && !$force) {
139  return $this;
140  }
141 
142  $this->_gccFile[$file] = $gccFile;
143  if ($combine == false) {
144  $this->excludeFiles[] = $file;
145  } elseif ($timestamp !== null)
146  $this->_timestamps['css'][$file] = $timestamp;
147  return parent::registerCssFile($file, $media);
148  }
149 
150  public function registerScriptFile($file, $position = self::POS_HEAD, $timestamp = null, $combine = true, $gccFile = false, $force = false)
151  {
152  if ($this->disableRegisterFiles && !$force) {
153  return $this;
154  }
155 
156  $this->_gccFile[$file] = $gccFile;
157  if ($combine == false) {
158  $this->excludeFiles[] = $file;
159  } elseif ($timestamp !== null)
160  $this->_timestamps['js'][$file] = $timestamp;
161  return parent::registerScriptFile($file);
162  }
163 
164  /**
165  * Will combine/compress JS and CSS if wanted/needed, and will continue with original
166  * renderHead afterwards
167  *
168  * @param <type> $output
169  */
170  public function renderHead(&$output)
171  {
172  if ($this->combineFiles) {
173  $this->combineJs = $this->combineCss = true;
174  }
175 
176  $this->processJs(parent::POS_HEAD);
177  $this->processCss();
178 
179  parent::renderHead($output);
180  }
181 
182  /**
183  * Will combine/compress JS if wanted/needed, and will continue with original
184  * renderBodyEnd afterwards
185  *
186  * @param <type> $output
187  */
188  public function renderBodyBegin(&$output)
189  {
190  $this->processJs(parent::POS_BEGIN);
191  parent::renderBodyBegin($output);
192  }
193 
194  /**
195  * Will combine/compress JS if wanted/needed, and will continue with original
196  * renderBodyEnd afterwards
197  *
198  * @param <type> $output
199  */
200  public function renderBodyEnd(&$output)
201  {
202  $this->processJs(parent::POS_END);
203  parent::renderBodyEnd($output);
204  }
205 
206 
207  /**
208  * @param <type> $output
209  * @param <type> $pos
210  */
211  private function processJs($pos)
212  {
213  if ($this->combineJs) {
214  if (isset($this->scriptFiles[$pos]) && count($this->scriptFiles[$pos]) !== 0) {
215  $jsFiles = array();
216  foreach ($this->scriptFiles[$pos] as $key => $file) {
217  if (!in_array($file, $this->excludeFiles)) {
218  $jsFiles[$key] = $file;
219  } else {
220  //add domain to url if exists
221  unset($this->scriptFiles[$pos][$key]);
222  $this->scriptFiles[$pos][$this->addSplitDomain($file)] = $this->addSplitDomain($file);
223  }
224  }
225  $this->combineAndCompress('js', $jsFiles);
226  }
227  }
228  }
229 
230  private function processCss()
231  {
232  if ($this->combineCss) {
233  if (count($this->cssFiles) !== 0) {
234  $cssFiles = array();
235  foreach ($this->cssFiles as $url => $media) {
236  if (!in_array($url, $this->excludeFiles)) {
237  $cssFiles[$media][$url] = $url;
238  } else {
239  //add domain to url if exists
240  unset($this->cssFiles[$url]);
241  $this->cssFiles[$this->addSplitDomain($url)] = $media;
242  }
243  }
244  foreach ($cssFiles as $media => $url) {
245  $this->combineAndCompress('css', $url);
246  }
247  }
248  }
249  }
250 
251  /**
252  * Performs the actual combining and compressing
253  *
254  * @param <type> $type
255  * @param <type> $urls
256  * @param <type> $pos
257  */
258  private function combineAndCompress($type, $urls)
259  {
260  $this->fileUrl or $this->fileUrl = $this->getCoreScriptUrl();
261  $this->basePath or $this->basePath = realpath($_SERVER['DOCUMENT_ROOT']);
262  $this->filePath or $this->filePath = $this->basePath . $this->fileUrl;
263 
264  $optionsHash = ($type == 'js') ?
265  md5($this->basePath . $this->compressCombinedJs . $this->ttlDays . $this->prefix) :
266  md5($this->basePath . $this->compressCombinedCss . $this->ttlDays . $this->prefix . serialize($this->cssMinFilters) . serialize($this->cssMinPlugins));
267  $combineHash = md5(implode('', $urls));
268  $fileName = $this->prefix . md5($combineHash . $optionsHash) . ".$type";
269  $timestampHash = !empty($this->_timestamps[$type]) ? md5(implode('', $this->_timestamps[$type])) : '';
270 
271  $timestampChanged = Yii::app()->cache->get($fileName) != $timestampHash;
272  $fileNotExists = (file_exists($this->filePath . '/' . $fileName)) ? false : true;
273  if ($fileNotExists || $timestampChanged) {
274  $this->garbageCollect($type);
275  $combinedFile = '';
276  foreach ($urls as $file) {
277  $combinedFile .= $this->_getFileContent($file);
278  }
279  if ($type == 'js' && $this->compressCombinedJs) {
280  $combinedFile = $this->minifyJs($combinedFile);
281  } elseif ($type == 'css' && $this->compressCombinedCss) {
282  $combinedFile = $this->minifyCss($combinedFile);
283  }
284  file_put_contents($this->filePath . '/' . $fileName, $combinedFile);
285  Yii::app()->cache->set($fileName, $timestampHash);
286  }
287  foreach ($urls as $url)
288  $this->scriptMap[basename($url)] = $this->addSplitDomain($this->fileUrl . '/' . $fileName);
289 
290  $this->remapScripts();
291  }
292 
293  private function garbageCollect($type)
294  {
295  $files = CFileHelper::findFiles($this->filePath, array('fileTypes' => array($type), 'level' => 0));
296  foreach ($files as $file) {
297  if (strpos($file, $this->prefix) !== false && $this->fileTTL($file)) {
298  unlink($file);
299  }
300  }
301  }
302 
303  /**
304  * Add domain name to url
305  *
306  * @param $url
307  */
308  private function addSplitDomain($url)
309  {
310  if ($this->splitDomainName && !$this->isRemoteFile($url)) {
311  return (Yii::app()->request->getIsSecureConnection() ? 'https' : 'http') . '://' . $this->splitDomainName . $url;
312  } else {
313  return $url;
314  }
315  }
316 
317  /**
318  * This function gets content of file depends of file type:
319  *
320  * If file is remote - download it
321  * If file from Gentics Content Connector - request it via API
322  * If file from webroot - read it as file
323  *
324  * @param string $file name of file
325  * @return string
326  */
327  private function _getFileContent($file)
328  {
329  if ($this->isRemoteFile($file)) {
330  return file_get_contents($file);
331  } elseif (isset($this->_gccFile[$file]) && $this->_gccFile[$file]) {
332  $content = Yii::app()->getModule('contentSource')->getContent($file);
333  if($content === false){
334  Yii::trace('CMS static file not exists '. $file, 'compressing');
335  return '';
336  } else {
337  return file_get_contents($content->getFile());
338  }
339  return file_get_contents($content->getFile());
340  } else {
341  return file_get_contents(Yii::getPathOfAlias('webroot') . '/' . $file);
342  }
343  }
344 
345  /**
346  * See if file is ready for deletion
347  *
348  * @param <type> $file
349  * @return bool
350  */
351  private function fileTTL($file)
352  {
353  if (!file_exists($file)) {
354  return false;
355  }
356  $ttl = $this->ttlDays * 60 * 60 * 24;
357  return ((fileatime($file) + $ttl) < time()) ? true : false;
358  }
359 
360  /**
361  * Minify javascript with JSMin
362  *
363  * @param <type> $js
364  */
365  private function minifyJs($js)
366  {
367  Yii::import($this->jsMinPath);
368  return JSMin::minify($js);
369  }
370 
371  /**
372  * Minify css with cssmin
373  *
374  * @param <type> $css
375  */
376  private function minifyCss($css)
377  {
378  Yii::import($this->jsMinPath);
379  Yii::import($this->cssMinPath);
380  return cssmin::minify($css, $this->cssMinFilters, $this->cssMinPlugins);
381  }
382 
383  /**
384  * See if file is on remote server
385  *
386  * @param <type> $file
387  */
388  private function isRemoteFile($file)
389  {
390  return (strpos($file, 'http://') === 0 || strpos($file, 'https://') === 0) ? true : false;
391  }
392 
393  private $_baseUrl;
394 
395  public function getCoreScriptUrl()
396  {
397  if ($this->_baseUrl !== null) {
398  return $this->_baseUrl;
399  } else {
400  return $this->_baseUrl = Yii::app()->getAssetManager()->publish(YII_PATH . '/web/js/source', true);
401  }
402  }
403 
404 
405 }