TYPO3 CMS  TYPO3_6-2
ResourceCompressor.php
Go to the documentation of this file.
1 <?php
3 
19 
27 
28  protected $targetDirectory = 'typo3temp/compressor/';
29 
30  protected $relativePath = '';
31 
32  protected $rootPath = '';
33 
34  protected $backPath = '';
35 
36  // gzipped versions are only created if $TYPO3_CONF_VARS[TYPO3_MODE]['compressionLevel'] is set
37  protected $createGzipped = FALSE;
38 
39  // default compression level is -1
40  protected $gzipCompressionLevel = -1;
41 
42  protected $htaccessTemplate = '<FilesMatch "\\.(js|css)(\\.gzip)?$">
43  <IfModule mod_expires.c>
44  ExpiresActive on
45  ExpiresDefault "access plus 7 days"
46  </IfModule>
47  FileETag MTime Size
48 </FilesMatch>';
49 
53  public function __construct() {
54  // we check for existence of our targetDirectory
55  if (!is_dir((PATH_site . $this->targetDirectory))) {
56  GeneralUtility::mkdir(PATH_site . $this->targetDirectory);
57  }
58  // if enabled, we check whether we should auto-create the .htaccess file
59  if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) {
60  // check whether .htaccess exists
61  $htaccessPath = PATH_site . $this->targetDirectory . '.htaccess';
62  if (!file_exists($htaccessPath)) {
63  GeneralUtility::writeFile($htaccessPath, $this->htaccessTemplate);
64  }
65  }
66  // decide whether we should create gzipped versions or not
67  $compressionLevel = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['compressionLevel'];
68  // we need zlib for gzencode()
69  if (extension_loaded('zlib') && $compressionLevel) {
70  $this->createGzipped = TRUE;
71  // $compressionLevel can also be TRUE
72  if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($compressionLevel)) {
73  $this->gzipCompressionLevel = (int)$compressionLevel;
74  }
75  }
76  $this->setInitialPaths();
77  }
78 
84  public function setInitialPaths() {
85  $this->setInitialRelativePath();
86  $this->setInitialRootPath();
87  $this->setInitialBackPath();
88  }
89 
95  protected function setInitialBackPath() {
96  $backPath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] : '';
97  $this->setBackPath($backPath);
98  }
99 
105  protected function setInitialRootPath() {
106  $rootPath = TYPO3_MODE === 'BE' ? PATH_typo3 : PATH_site;
107  $this->setRootPath($rootPath);
108  }
109 
115  protected function setInitialRelativePath() {
116  $relativePath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] . '../' : '';
118  }
119 
126  public function setRelativePath($relativePath) {
127  if (is_string($relativePath)) {
128  $this->relativePath = $relativePath;
129  }
130  }
131 
138  public function setRootPath($rootPath) {
139  if (is_string($rootPath)) {
140  $this->rootPath = $rootPath;
141  }
142  }
143 
150  public function setBackPath($backPath) {
151  if (is_string($backPath)) {
152  $this->backPath = $backPath;
153  }
154  }
155 
166  public function concatenateCssFiles(array $cssFiles, array $options = array()) {
167  $filesToInclude = array();
168  foreach ($cssFiles as $key => $fileOptions) {
169  // no concatenation allowed for this file, so continue
170  if (!empty($fileOptions['excludeFromConcatenation'])) {
171  continue;
172  }
173  // we remove BACK_PATH from $filename, so make it relative to root path
174  $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
175  // if $options['baseDirectories'] set, we only include files below these directories
176  if ((!isset($options['baseDirectories']) || $this->checkBaseDirectory($filenameFromMainDir, array_merge($options['baseDirectories'], array($this->targetDirectory)))) && $fileOptions['media'] === 'all') {
177  if ($fileOptions['forceOnTop']) {
178  array_unshift($filesToInclude, $filenameFromMainDir);
179  } else {
180  $filesToInclude[] = $filenameFromMainDir;
181  }
182  // remove the file from the incoming file array
183  unset($cssFiles[$key]);
184  }
185  }
186  if (!empty($filesToInclude)) {
187  $targetFile = $this->createMergedCssFile($filesToInclude);
188  $targetFileRelative = $this->relativePath . $targetFile;
189  $concatenatedOptions = array(
190  'file' => $targetFileRelative,
191  'rel' => 'stylesheet',
192  'media' => 'all',
193  'compress' => TRUE,
194  'excludeFromConcatenation' => TRUE,
195  'forceOnTop' => FALSE,
196  'allWrap' => ''
197  );
198  // place the merged stylesheet on top of the stylesheets
199  $cssFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $cssFiles);
200  }
201  return $cssFiles;
202  }
203 
210  public function concatenateJsFiles(array $jsFiles) {
211  $filesToInclude = array();
212  foreach ($jsFiles as $key => $fileOptions) {
213  // invalid section found or no concatenation allowed, so continue
214  if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
215  continue;
216  }
217  if (!isset($filesToInclude[$fileOptions['section']])) {
218  $filesToInclude[$fileOptions['section']] = array();
219  }
220  // we remove BACK_PATH from $filename, so make it relative to root path
221  $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
222  if ($fileOptions['forceOnTop']) {
223  array_unshift($filesToInclude[$fileOptions['section']], $filenameFromMainDir);
224  } else {
225  $filesToInclude[$fileOptions['section']][] = $filenameFromMainDir;
226  }
227  // remove the file from the incoming file array
228  unset($jsFiles[$key]);
229  }
230  if (!empty($filesToInclude)) {
231  foreach ($filesToInclude as $section => $files) {
232  $targetFile = $this->createMergedJsFile($files);
233  $targetFileRelative = $this->relativePath . $targetFile;
234  $concatenatedOptions = array(
235  'file' => $targetFileRelative,
236  'type' => 'text/javascript',
237  'section' => $section,
238  'compress' => TRUE,
239  'excludeFromConcatenation' => TRUE,
240  'forceOnTop' => FALSE,
241  'allWrap' => ''
242  );
243  // place the merged javascript on top of the JS files
244  $jsFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $jsFiles);
245  }
246  }
247  return $jsFiles;
248  }
249 
256  protected function createMergedCssFile(array $filesToInclude) {
257  return $this->createMergedFile($filesToInclude, 'css');
258  }
259 
266  protected function createMergedJsFile(array $filesToInclude) {
267  return $this->createMergedFile($filesToInclude, 'js');
268  }
269 
279  protected function createMergedFile(array $filesToInclude, $type = 'css') {
280  // Get file type
281  $type = strtolower(trim($type, '. '));
282  if (empty($type)) {
283  throw new \InvalidArgumentException('Error in TYPO3\\CMS\\Core\\Resource\\ResourceCompressor: No valid file type given for merged file', 1308957498);
284  }
285  // we add up the filenames, filemtimes and filsizes to later build a checksum over
286  // it and include it in the temporary file name
287  $unique = '';
288  foreach ($filesToInclude as $key => $filename) {
289  if (GeneralUtility::isValidUrl($filename)) {
290  // check if it is possibly a local file with fully qualified URL
291  if (GeneralUtility::isOnCurrentHost($filename) &&
293  $filename,
294  GeneralUtility::getIndpEnv('TYPO3_SITE_URL')
295  )
296  ) {
297  // attempt to turn it into a local file path
298  $localFilename = substr($filename, strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_URL')));
299  if (@is_file(GeneralUtility::resolveBackPath($this->rootPath . $localFilename))) {
300  $filesToInclude[$key] = $localFilename;
301  } else {
302  $filesToInclude[$key] = $this->retrieveExternalFile($filename);
303  }
304  } else {
305  $filesToInclude[$key] = $this->retrieveExternalFile($filename);
306  }
307  $filename = $filesToInclude[$key];
308  }
309  $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $filename);
310  if (@file_exists($filenameAbsolute)) {
311  $fileStatus = stat($filenameAbsolute);
312  $unique .= $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
313  } else {
314  $unique .= $filenameAbsolute;
315  }
316  }
317  $targetFile = $this->targetDirectory . 'merged-' . md5($unique) . '.' . $type;
318  // if the file doesn't already exist, we create it
319  if (!file_exists((PATH_site . $targetFile))) {
320  $concatenated = '';
321  // concatenate all the files together
322  foreach ($filesToInclude as $filename) {
323  $contents = GeneralUtility::getUrl(GeneralUtility::resolveBackPath($this->rootPath . $filename));
324  // remove any UTF-8 byte order mark (BOM) from files
325  if (GeneralUtility::isFirstPartOfStr($contents, "\xEF\xBB\xBF")) {
326  $contents = substr($contents, 3);
327  }
328  // only fix paths if files aren't already in typo3temp (already processed)
329  if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
330  $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
331  }
332  $concatenated .= LF . $contents;
333  }
334  // move @charset, @import and @namespace statements to top of new file
335  if ($type === 'css') {
336  $concatenated = $this->cssFixStatements($concatenated);
337  }
338  GeneralUtility::writeFile(PATH_site . $targetFile, $concatenated);
339  }
340  return $targetFile;
341  }
342 
349  public function compressCssFiles(array $cssFiles) {
350  $filesAfterCompression = array();
351  foreach ($cssFiles as $key => $fileOptions) {
352  // if compression is enabled
353  if ($fileOptions['compress']) {
354  $filename = $this->compressCssFile($fileOptions['file']);
355  $fileOptions['compress'] = FALSE;
356  $fileOptions['file'] = $filename;
357  $filesAfterCompression[$filename] = $fileOptions;
358  } else {
359  $filesAfterCompression[$key] = $fileOptions;
360  }
361  }
362  return $filesAfterCompression;
363  }
364 
377  public function compressCssFile($filename) {
378  // generate the unique name of the file
379  $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
380  if (@file_exists($filenameAbsolute)) {
381  $fileStatus = stat($filenameAbsolute);
382  $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
383  } else {
384  $unique = $filenameAbsolute;
385  }
386 
387  $pathinfo = PathUtility::pathinfo($filename);
388  $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css';
389  // only create it, if it doesn't exist, yet
390  if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
391  $contents = GeneralUtility::getUrl($filenameAbsolute);
392  // Perform some safe CSS optimizations.
393  $contents = str_replace(CR, '', $contents);
394  // Strip any and all carriage returns.
395  // Match and process strings, comments and everything else, one chunk at a time.
396  // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
397  $contents = preg_replace_callback('%
398  # One-regex-to-rule-them-all! - version: 20100220_0100
399  # Group 1: Match a double quoted string.
400  ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") | # or...
401  # Group 2: Match a single quoted string.
402  (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') | # or...
403  # Group 3: Match a regular non-MacIE5-hack comment.
404  (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) | # or...
405  # Group 4: Match a MacIE5-type1 comment.
406  (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) | # or...
407  # Group 5: Match a MacIE5-type2 comment.
408  (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/)) # folllowed by...
409  # Group 6: Match everything up to final closing regular comment
410  ([^/]*+(?:(?!\\*)/[^/]*+)*?)
411  # Group 7: Match final closing regular comment
412  (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) | # or...
413  # Group 8: Match regular non-string, non-comment text.
414  ([^"\'/]*+(?:(?!/\\*)/[^"\'/]*+)*+)
415  %Ssx', array('self', 'compressCssPregCallback'), $contents);
416  // Do it!
417  $contents = preg_replace('/^\\s++/', '', $contents);
418  // Strip leading whitespace.
419  $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
420 ', $contents);
421  // Consolidate multi-lines space.
422  $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
423 ', $contents);
424  // Ensure file ends in newline.
425  // we have to fix relative paths, if we aren't working on a file in our target directory
426  if (strpos($filename, $this->targetDirectory) === FALSE) {
427  $filenameRelativeToMainDir = substr($filename, strlen($this->backPath));
428  $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filenameRelativeToMainDir) . '/');
429  }
430  $this->writeFileAndCompressed($targetFile, $contents);
431  }
432  return $this->relativePath . $this->returnFileReference($targetFile);
433  }
434 
442  static public function compressCssPregCallback($matches) {
443  if ($matches[1]) {
444  // Group 1: Double quoted string.
445  return $matches[1];
446  } elseif ($matches[2]) {
447  // Group 2: Single quoted string.
448  return $matches[2];
449  } elseif ($matches[3]) {
450  // Group 3: Regular non-MacIE5-hack comment.
451  return '
452 ';
453  } elseif ($matches[4]) {
454  // Group 4: MacIE5-hack-type-1 comment.
455  return '
456 /*\\T1*/
457 ';
458  } elseif ($matches[5]) {
459  // Group 5,6,7: MacIE5-hack-type-2 comment
460  $matches[6] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[6]);
461  // Clean pre-punctuation.
462  $matches[6] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[6]);
463  // Clean post-punctuation.
464  $matches[6] = preg_replace('/;?\\}/S', '}
465 ', $matches[6]);
466  // Add a touch of formatting.
467  return '
468 /*T2\\*/' . $matches[6] . '
469 /*T2E*/
470 ';
471  } elseif (isset($matches[8])) {
472  // Group 8: Non-string, non-comment. Safe to clean whitespace here.
473  $matches[8] = preg_replace('/^\\s++/', '', $matches[8]);
474  // Strip all leading whitespace.
475  $matches[8] = preg_replace('/\\s++$/', '', $matches[8]);
476  // Strip all trailing whitespace.
477  $matches[8] = preg_replace('/\\s{2,}+/', ' ', $matches[8]);
478  // Consolidate multiple whitespace.
479  $matches[8] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[8]);
480  // Clean pre-punctuation.
481  $matches[8] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[8]);
482  // Clean post-punctuation.
483  $matches[8] = preg_replace('/;?\\}/S', '}
484 ', $matches[8]);
485  // Add a touch of formatting.
486  return $matches[8];
487  }
488  return $matches[0] . '
489 /* ERROR! Unexpected _proccess_css_minify() parameter */
490 ';
491  }
492 
499  public function compressJsFiles(array $jsFiles) {
500  $filesAfterCompression = array();
501  foreach ($jsFiles as $fileName => $fileOptions) {
502  // If compression is enabled
503  if ($fileOptions['compress']) {
504  $compressedFilename = $this->compressJsFile($fileOptions['file']);
505  $fileOptions['compress'] = FALSE;
506  $fileOptions['file'] = $compressedFilename;
507  $filesAfterCompression[$compressedFilename] = $fileOptions;
508  } else {
509  $filesAfterCompression[$fileName] = $fileOptions;
510  }
511  }
512  return $filesAfterCompression;
513  }
514 
521  public function compressJsFile($filename) {
522  // generate the unique name of the file
523  $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
524  if (@file_exists($filenameAbsolute)) {
525  $fileStatus = stat($filenameAbsolute);
526  $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
527  } else {
528  $unique = $filenameAbsolute;
529  }
530  $pathinfo = PathUtility::pathinfo($filename);
531  $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
532  // only create it, if it doesn't exist, yet
533  if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
534  $contents = GeneralUtility::getUrl($filenameAbsolute);
535  $this->writeFileAndCompressed($targetFile, $contents);
536  }
537  return $this->relativePath . $this->returnFileReference($targetFile);
538  }
539 
546  protected function getFilenameFromMainDir($filename) {
547  // if BACK_PATH is empty return $filename
548  if (empty($this->backPath)) {
549  return $filename;
550  }
551  // if the file exists in the root path, just return the $filename
552  if (strpos($filename, $this->backPath) === 0) {
553  $file = str_replace($this->backPath, '', $filename);
554  if (is_file(GeneralUtility::resolveBackPath($this->rootPath . $file))) {
555  return $file;
556  }
557  }
558  // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
559  if (is_file(realpath(PATH_site . TYPO3_mainDir . $filename))) {
560  $filename = TYPO3_mainDir . $filename;
561  }
562  // build the file path relatively to the PATH_site
563  $backPath = str_replace(TYPO3_mainDir, '', $this->backPath);
564  $file = str_replace($backPath, '', $filename);
565  if (substr($file, 0, 3) === '../') {
566  $file = GeneralUtility::resolveBackPath(PATH_typo3 . $file);
567  } else {
568  $file = PATH_site . $file;
569  }
570  // check if the file exists, and if so, return the path relative to TYPO3_mainDir
571  if (is_file($file)) {
572  $mainDirDepth = substr_count(TYPO3_mainDir, '/');
573  return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file);
574  }
575  // none of above conditions were met, fallback to default behaviour
576  return substr($filename, strlen($this->backPath));
577  }
578 
586  protected function checkBaseDirectory($filename, array $baseDirectories) {
587  foreach ($baseDirectories as $baseDirectory) {
588  // check, if $filename starts with base directory
589  if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
590  return TRUE;
591  }
592  }
593  return FALSE;
594  }
595 
603  protected function cssFixRelativeUrlPaths($contents, $oldDir) {
604  $mainDir = TYPO3_MODE === 'BE' ? TYPO3_mainDir : '';
605  $newDir = '../../' . $mainDir . $oldDir;
606  // Replace "url()" paths
607  if (stripos($contents, 'url') !== FALSE) {
608  $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
609  $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
610  }
611  // Replace "@import" paths
612  if (stripos($contents, '@import') !== FALSE) {
613  $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
614  $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
615  }
616  return $contents;
617  }
618 
628  protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|') {
629  $matches = array();
630  $replacements = array();
631  $wrap = explode('|', $wrap);
632  preg_match_all($regex, $contents, $matches);
633  foreach ($matches[2] as $matchCount => $match) {
634  // remove '," or white-spaces around
635  $match = trim($match, '\'" ');
636  // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
637  if (strpos($match, ':') === FALSE && !preg_match('/url\\s*\\(/i', $match)) {
638  $newPath = GeneralUtility::resolveBackPath($newDir . $match);
639  $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
640  }
641  }
642  // replace URL paths in content
643  if (!empty($replacements)) {
644  $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
645  }
646  return $contents;
647  }
648 
656  protected function cssFixStatements($contents) {
657  $matches = array();
658  $comment = LF . '/* moved by compressor */' . LF;
659  // nothing to do, so just return contents
660  if (stripos($contents, '@charset') === FALSE && stripos($contents, '@import') === FALSE && stripos($contents, '@namespace') === FALSE) {
661  return $contents;
662  }
663  $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\'\\)]+["\']?\\s*\\)?\\s*;/i';
664  preg_match_all($regex, $contents, $matches);
665  if (!empty($matches[0])) {
666  // remove existing statements
667  $contents = str_replace($matches[0], '', $contents);
668  // add statements to the top of contents in the order they occur in original file
669  $contents = $comment . implode($comment, $matches[0]) . LF . trim($contents);
670  }
671  return $contents;
672  }
673 
681  protected function writeFileAndCompressed($filename, $contents) {
682  // write uncompressed file
683  GeneralUtility::writeFile(PATH_site . $filename, $contents);
684  if ($this->createGzipped) {
685  // create compressed version
686  GeneralUtility::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
687  }
688  }
689 
697  protected function returnFileReference($filename) {
698  // if the client accepts gzip and we can create gzipped files, we give him compressed versions
699  if ($this->createGzipped && strpos(GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) {
700  return $filename . '.gzip';
701  } else {
702  return $filename;
703  }
704  }
705 
712  protected function retrieveExternalFile($url) {
713  $externalContent = GeneralUtility::getUrl($url);
714  $filename = $this->targetDirectory . 'external-' . md5($url);
715  // write only if file does not exist and md5 of the content is not the same as fetched one
716  if (!file_exists(PATH_site . $filename) &&
717  (md5($externalContent) !== md5(GeneralUtility::getUrl(PATH_site . $filename)))
718  ) {
719  GeneralUtility::writeFile(PATH_site . $filename, $externalContent);
720  }
721  return $filename;
722  }
723 
724 }
static writeFile($file, $content, $changePermissions=FALSE)
static isFirstPartOfStr($str, $partStr)
findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap='|')
const TYPO3_MODE
Definition: init.php:40
static pathinfo($path, $options=NULL)
static getUrl($url, $includeHeader=0, $requestHeaders=FALSE, &$report=NULL)
checkBaseDirectory($filename, array $baseDirectories)
createMergedFile(array $filesToInclude, $type='css')
if(!defined('TYPO3_MODE')) $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'][]
concatenateCssFiles(array $cssFiles, array $options=array())