[BUGFIX] CSS3 function calc must retain whitespace
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ResourceCompressor.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\PathUtility;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19
20 /**
21 * Compressor
22 * This merges and compresses CSS and JavaScript files of the TYPO3 Backend.
23 *
24 * @author Steffen Gebert <steffen@steffen-gebert.de>
25 */
26 class ResourceCompressor {
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
50 /**
51 * Constructor
52 */
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
79 /**
80 * Sets initial values for paths.
81 *
82 * @return void
83 */
84 public function setInitialPaths() {
85 $this->setInitialRelativePath();
86 $this->setInitialRootPath();
87 $this->setInitialBackPath();
88 }
89
90 /**
91 * Sets relative back path
92 *
93 * @return void
94 */
95 protected function setInitialBackPath() {
96 $backPath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] : '';
97 $this->setBackPath($backPath);
98 }
99
100 /**
101 * Sets absolute path to working directory
102 *
103 * @return void
104 */
105 protected function setInitialRootPath() {
106 $rootPath = TYPO3_MODE === 'BE' ? PATH_typo3 : PATH_site;
107 $this->setRootPath($rootPath);
108 }
109
110 /**
111 * Sets relative path to PATH_site
112 *
113 * @return void
114 */
115 protected function setInitialRelativePath() {
116 $relativePath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] . '../' : '';
117 $this->setRelativePath($relativePath);
118 }
119
120 /**
121 * Sets relative path to PATH_site
122 *
123 * @param string $relativePath Relative path to site root
124 * @return void
125 */
126 public function setRelativePath($relativePath) {
127 if (is_string($relativePath)) {
128 $this->relativePath = $relativePath;
129 }
130 }
131
132 /**
133 * Sets absolute path to working directory
134 *
135 * @param string $rootPath Absolute path
136 * @return void
137 */
138 public function setRootPath($rootPath) {
139 if (is_string($rootPath)) {
140 $this->rootPath = $rootPath;
141 }
142 }
143
144 /**
145 * Sets relative back path
146 *
147 * @param string $backPath Back path
148 * @return void
149 */
150 public function setBackPath($backPath) {
151 if (is_string($backPath)) {
152 $this->backPath = $backPath;
153 }
154 }
155
156 /**
157 * Concatenates the Stylesheet files
158 *
159 * Options:
160 * baseDirectories If set, only include files below one of the base directories
161 *
162 * @param array $cssFiles CSS files to process
163 * @param array $options Additional options
164 * @return array CSS files
165 */
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
204 /**
205 * Concatenates the JavaScript files
206 *
207 * @param array $jsFiles JavaScript files to process
208 * @return array JS files
209 */
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
250 /**
251 * Creates a merged CSS file
252 *
253 * @param array $filesToInclude Files which should be merged, paths relative to root path
254 * @return mixed Filename of the merged file
255 */
256 protected function createMergedCssFile(array $filesToInclude) {
257 return $this->createMergedFile($filesToInclude, 'css');
258 }
259
260 /**
261 * Creates a merged JS file
262 *
263 * @param array $filesToInclude Files which should be merged, paths relative to root path
264 * @return mixed Filename of the merged file
265 */
266 protected function createMergedJsFile(array $filesToInclude) {
267 return $this->createMergedFile($filesToInclude, 'js');
268 }
269
270 /**
271 * Creates a merged file with given file type
272 *
273 * @param array $filesToInclude Files which should be merged, paths relative to root path
274 * @param string $type File type
275 *
276 * @throws \InvalidArgumentException
277 * @return mixed Filename of the merged file
278 */
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) &&
292 GeneralUtility::isFirstPartOfStr(
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 // only fix paths if files aren't already in typo3temp (already processed)
325 if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
326 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
327 }
328 $concatenated .= LF . $contents;
329 }
330 // move @charset, @import and @namespace statements to top of new file
331 if ($type === 'css') {
332 $concatenated = $this->cssFixStatements($concatenated);
333 }
334 GeneralUtility::writeFile(PATH_site . $targetFile, $concatenated);
335 }
336 return $targetFile;
337 }
338
339 /**
340 * Compress multiple css files
341 *
342 * @param array $cssFiles The files to compress (array key = filename), relative to requested page
343 * @return array The CSS files after compression (array key = new filename), relative to requested page
344 */
345 public function compressCssFiles(array $cssFiles) {
346 $filesAfterCompression = array();
347 foreach ($cssFiles as $key => $fileOptions) {
348 // if compression is enabled
349 if ($fileOptions['compress']) {
350 $filename = $this->compressCssFile($fileOptions['file']);
351 $fileOptions['compress'] = FALSE;
352 $fileOptions['file'] = $filename;
353 $filesAfterCompression[$filename] = $fileOptions;
354 } else {
355 $filesAfterCompression[$key] = $fileOptions;
356 }
357 }
358 return $filesAfterCompression;
359 }
360
361 /**
362 * Compresses a CSS file
363 *
364 * Options:
365 * baseDirectories If set, only include files below one of the base directories
366 *
367 * removes comments and whitespaces
368 * Adopted from http://drupal.org/files/issues/minify_css.php__1.txt
369 *
370 * @param string $filename Source filename, relative to requested page
371 * @return string Compressed filename, relative to requested page
372 */
373 public function compressCssFile($filename) {
374 // generate the unique name of the file
375 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
376 if (@file_exists($filenameAbsolute)) {
377 $fileStatus = stat($filenameAbsolute);
378 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
379 } else {
380 $unique = $filenameAbsolute;
381 }
382
383 $pathinfo = PathUtility::pathinfo($filename);
384 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css';
385 // only create it, if it doesn't exist, yet
386 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
387 $contents = GeneralUtility::getUrl($filenameAbsolute);
388 // Compress CSS Content
389 $contents = $this->compressCssString($contents);
390 // Ensure file ends in newline.
391 // we have to fix relative paths, if we aren't working on a file in our target directory
392 if (strpos($filename, $this->targetDirectory) === FALSE) {
393 $filenameRelativeToMainDir = substr($filename, strlen($this->backPath));
394 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filenameRelativeToMainDir) . '/');
395 }
396 $this->writeFileAndCompressed($targetFile, $contents);
397 }
398 return $this->relativePath . $this->returnFileReference($targetFile);
399 }
400
401 /**
402 * Callback function for preg_replace
403 *
404 * @see compressCssFile
405 * @param array $matches
406 * @return string the compressed string
407 */
408 static public function compressCssPregCallback($matches) {
409 if ($matches[1]) {
410 // Group 1: Double quoted string.
411 return $matches[1];
412 } elseif ($matches[2]) {
413 // Group 2: Single quoted string.
414 return $matches[2];
415 } elseif ($matches[3]) {
416 // Group 3: Regular non-MacIE5-hack comment.
417 return '
418 ';
419 } elseif ($matches[4]) {
420 // Group 4: MacIE5-hack-type-1 comment.
421 return '
422 /*\\T1*/
423 ';
424 } elseif ($matches[5]) {
425 // Group 5,6,7: MacIE5-hack-type-2 comment
426 $matches[6] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[6]);
427 // Clean pre-punctuation.
428 $matches[6] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[6]);
429 // Clean post-punctuation.
430 $matches[6] = preg_replace('/;?\\}/S', '}
431 ', $matches[6]);
432 // Add a touch of formatting.
433 return '
434 /*T2\\*/' . $matches[6] . '
435 /*T2E*/
436 ';
437 } elseif ($matches[8]) {
438 // Group 8: calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
439 return 'calc' . $matches[8];
440 } elseif (isset($matches[9])) {
441 // Group 9: Non-string, non-comment. Safe to clean whitespace here.
442 $matches[9] = preg_replace('/^\\s++/', '', $matches[9]);
443 // Strip all leading whitespace.
444 $matches[9] = preg_replace('/\\s++$/', '', $matches[9]);
445 // Strip all trailing whitespace.
446 $matches[9] = preg_replace('/\\s{2,}+/', ' ', $matches[9]);
447 // Consolidate multiple whitespace.
448 $matches[9] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[9]);
449 // Clean pre-punctuation.
450 $matches[9] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[9]);
451 // Clean post-punctuation.
452 $matches[9] = preg_replace('/;?\\}/S', '}
453 ', $matches[9]);
454 // Add a touch of formatting.
455 return $matches[9];
456 }
457 return $matches[0] . '
458 /* ERROR! Unexpected _proccess_css_minify() parameter */
459 ';
460 }
461
462 /**
463 * Compress multiple javascript files
464 *
465 * @param array $jsFiles The files to compress (array key = filename), relative to requested page
466 * @return array The js files after compression (array key = new filename), relative to requested page
467 */
468 public function compressJsFiles(array $jsFiles) {
469 $filesAfterCompression = array();
470 foreach ($jsFiles as $fileName => $fileOptions) {
471 // If compression is enabled
472 if ($fileOptions['compress']) {
473 $compressedFilename = $this->compressJsFile($fileOptions['file']);
474 $fileOptions['compress'] = FALSE;
475 $fileOptions['file'] = $compressedFilename;
476 $filesAfterCompression[$compressedFilename] = $fileOptions;
477 } else {
478 $filesAfterCompression[$fileName] = $fileOptions;
479 }
480 }
481 return $filesAfterCompression;
482 }
483
484 /**
485 * Compresses a javascript file
486 *
487 * @param string $filename Source filename, relative to requested page
488 * @return string Filename of the compressed file, relative to requested page
489 */
490 public function compressJsFile($filename) {
491 // generate the unique name of the file
492 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
493 if (@file_exists($filenameAbsolute)) {
494 $fileStatus = stat($filenameAbsolute);
495 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
496 } else {
497 $unique = $filenameAbsolute;
498 }
499 $pathinfo = PathUtility::pathinfo($filename);
500 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
501 // only create it, if it doesn't exist, yet
502 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
503 $contents = GeneralUtility::getUrl($filenameAbsolute);
504 $this->writeFileAndCompressed($targetFile, $contents);
505 }
506 return $this->relativePath . $this->returnFileReference($targetFile);
507 }
508
509 /**
510 * Finds the relative path to a file, relative to the root path.
511 *
512 * @param string $filename the name of the file
513 * @return string the path to the file relative to the root path
514 */
515 protected function getFilenameFromMainDir($filename) {
516 // if BACK_PATH is empty return $filename
517 if (empty($this->backPath)) {
518 return $filename;
519 }
520 // if the file exists in the root path, just return the $filename
521 if (strpos($filename, $this->backPath) === 0) {
522 $file = str_replace($this->backPath, '', $filename);
523 if (is_file(GeneralUtility::resolveBackPath($this->rootPath . $file))) {
524 return $file;
525 }
526 }
527 // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
528 if (is_file(realpath(PATH_site . TYPO3_mainDir . $filename))) {
529 $filename = TYPO3_mainDir . $filename;
530 }
531 // build the file path relatively to the PATH_site
532 $backPath = str_replace(TYPO3_mainDir, '', $this->backPath);
533 $file = str_replace($backPath, '', $filename);
534 if (substr($file, 0, 3) === '../') {
535 $file = GeneralUtility::resolveBackPath(PATH_typo3 . $file);
536 } else {
537 $file = PATH_site . $file;
538 }
539 // check if the file exists, and if so, return the path relative to TYPO3_mainDir
540 if (is_file($file)) {
541 $mainDirDepth = substr_count(TYPO3_mainDir, '/');
542 return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file);
543 }
544 // none of above conditions were met, fallback to default behaviour
545 return substr($filename, strlen($this->backPath));
546 }
547
548 /**
549 * Decides whether a file comes from one of the baseDirectories
550 *
551 * @param string $filename Filename
552 * @param array $baseDirectories Base directories
553 * @return boolean File belongs to a base directory or not
554 */
555 protected function checkBaseDirectory($filename, array $baseDirectories) {
556 foreach ($baseDirectories as $baseDirectory) {
557 // check, if $filename starts with base directory
558 if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
559 return TRUE;
560 }
561 }
562 return FALSE;
563 }
564
565 /**
566 * Fixes the relative paths inside of url() references in CSS files
567 *
568 * @param string $contents Data to process
569 * @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
570 * @return string Processed data
571 */
572 protected function cssFixRelativeUrlPaths($contents, $oldDir) {
573 $mainDir = TYPO3_MODE === 'BE' ? TYPO3_mainDir : '';
574 $newDir = '../../' . $mainDir . $oldDir;
575 // Replace "url()" paths
576 if (stripos($contents, 'url') !== FALSE) {
577 $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
578 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
579 }
580 // Replace "@import" paths
581 if (stripos($contents, '@import') !== FALSE) {
582 $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
583 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
584 }
585 return $contents;
586 }
587
588 /**
589 * Finds and replaces all URLs by using a given regex
590 *
591 * @param string $contents Data to process
592 * @param string $regex Regex used to find URLs in content
593 * @param string $newDir Path to prepend to the original file
594 * @param string $wrap Wrap around replaced values
595 * @return string Processed data
596 */
597 protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|') {
598 $matches = array();
599 $replacements = array();
600 $wrap = explode('|', $wrap);
601 preg_match_all($regex, $contents, $matches);
602 foreach ($matches[2] as $matchCount => $match) {
603 // remove '," or white-spaces around
604 $match = trim($match, '\'" ');
605 // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
606 if (strpos($match, ':') === FALSE && !preg_match('/url\\s*\\(/i', $match)) {
607 $newPath = GeneralUtility::resolveBackPath($newDir . $match);
608 $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
609 }
610 }
611 // replace URL paths in content
612 if (!empty($replacements)) {
613 $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
614 }
615 return $contents;
616 }
617
618 /**
619 * Moves @charset, @import and @namespace statements to the top of
620 * the content, because they must occur before all other CSS rules
621 *
622 * @param string $contents Data to process
623 * @return string Processed data
624 */
625 protected function cssFixStatements($contents) {
626 $matches = array();
627 $comment = LF . '/* moved by compressor */' . LF;
628 // nothing to do, so just return contents
629 if (stripos($contents, '@charset') === FALSE && stripos($contents, '@import') === FALSE && stripos($contents, '@namespace') === FALSE) {
630 return $contents;
631 }
632 $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\'\\)]+["\']?\\s*\\)?\\s*;/i';
633 preg_match_all($regex, $contents, $matches);
634 if (!empty($matches[0])) {
635 // remove existing statements
636 $contents = str_replace($matches[0], '', $contents);
637 // add statements to the top of contents in the order they occur in original file
638 $contents = $comment . implode($comment, $matches[0]) . LF . trim($contents);
639 }
640 return $contents;
641 }
642
643 /**
644 * Writes $contents into file $filename together with a gzipped version into $filename.gz
645 *
646 * @param string $filename Target filename
647 * @param string $contents File contents
648 * @return void
649 */
650 protected function writeFileAndCompressed($filename, $contents) {
651 // write uncompressed file
652 GeneralUtility::writeFile(PATH_site . $filename, $contents);
653 if ($this->createGzipped) {
654 // create compressed version
655 GeneralUtility::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
656 }
657 }
658
659 /**
660 * Decides whether a client can deal with gzipped content or not and returns the according file name,
661 * based on HTTP_ACCEPT_ENCODING
662 *
663 * @param string $filename File name
664 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING
665 */
666 protected function returnFileReference($filename) {
667 // if the client accepts gzip and we can create gzipped files, we give him compressed versions
668 if ($this->createGzipped && strpos(GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) {
669 return $filename . '.gzip';
670 } else {
671 return $filename;
672 }
673 }
674
675 /**
676 * Retrieves an external file and stores it locally.
677 *
678 * @param string $url
679 * @return string Temporary local filename for the externally-retrieved file
680 */
681 protected function retrieveExternalFile($url) {
682 $externalContent = GeneralUtility::getUrl($url);
683 $filename = $this->targetDirectory . 'external-' . md5($url);
684 // write only if file does not exist and md5 of the content is not the same as fetched one
685 if (!file_exists(PATH_site . $filename) &&
686 (md5($externalContent) !== md5(GeneralUtility::getUrl(PATH_site . $filename)))
687 ) {
688 GeneralUtility::writeFile(PATH_site . $filename, $externalContent);
689 }
690 return $filename;
691 }
692
693 /**
694 * Compress a CSS string by removing comments and whitespace characters
695 *
696 * @param string $contents
697 * @return string
698 */
699 protected function compressCssString($contents) {
700 // Perform some safe CSS optimizations.
701 $contents = str_replace(CR, '', $contents);
702 // Strip any and all carriage returns.
703 // Match and process strings, comments and everything else, one chunk at a time.
704 // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
705 $contents = preg_replace_callback('%
706 # One-regex-to-rule-them-all! - version: 20100220_0100
707 # Group 1: Match a double quoted string.
708 ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") | # or...
709 # Group 2: Match a single quoted string.
710 (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') | # or...
711 # Group 3: Match a regular non-MacIE5-hack comment.
712 (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) | # or...
713 # Group 4: Match a MacIE5-type1 comment.
714 (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) | # or...
715 # Group 5: Match a MacIE5-type2 comment.
716 (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/)) # folllowed by...
717 # Group 6: Match everything up to final closing regular comment
718 ([^/]*+(?:(?!\\*)/[^/]*+)*?)
719 # Group 7: Match final closing regular comment
720 (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) | # or...
721 # Group 8: Match a calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
722 (?:calc(\\((?:(?:[^\\(\\)]+)|(?8))*+\\))) | # or...
723 # Group 9: Match regular non-string, non-comment text.
724 ((?:[^"\'/](?!calc))*+(?:(?!/\\*)/(?:[^"\'/](?!calc))*+)*+)
725 %Ssx', array('self', 'compressCssPregCallback'), $contents);
726 // Do it!
727 $contents = preg_replace('/^\\s++/', '', $contents);
728 // Strip leading whitespace.
729 $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
730 ', $contents);
731 // Consolidate multi-lines space.
732 $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
733 ', $contents);
734
735 return $contents;
736 }
737
738 }