651f458762e0e52edfa56dd3f754ff9b92d71d1f
[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 class ResourceCompressor {
25
26 protected $targetDirectory = 'typo3temp/compressor/';
27
28 protected $relativePath = '';
29
30 protected $rootPath = '';
31
32 protected $backPath = '';
33
34 // gzipped versions are only created if $TYPO3_CONF_VARS[TYPO3_MODE]['compressionLevel'] is set
35 protected $createGzipped = FALSE;
36
37 // default compression level is -1
38 protected $gzipCompressionLevel = -1;
39
40 protected $htaccessTemplate = '<FilesMatch "\\.(js|css)(\\.gzip)?$">
41 <IfModule mod_expires.c>
42 ExpiresActive on
43 ExpiresDefault "access plus 7 days"
44 </IfModule>
45 FileETag MTime Size
46 </FilesMatch>';
47
48 /**
49 * Constructor
50 */
51 public function __construct() {
52 // we check for existence of our targetDirectory
53 if (!is_dir((PATH_site . $this->targetDirectory))) {
54 GeneralUtility::mkdir(PATH_site . $this->targetDirectory);
55 }
56 // if enabled, we check whether we should auto-create the .htaccess file
57 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) {
58 // check whether .htaccess exists
59 $htaccessPath = PATH_site . $this->targetDirectory . '.htaccess';
60 if (!file_exists($htaccessPath)) {
61 GeneralUtility::writeFile($htaccessPath, $this->htaccessTemplate);
62 }
63 }
64 // decide whether we should create gzipped versions or not
65 $compressionLevel = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['compressionLevel'];
66 // we need zlib for gzencode()
67 if (extension_loaded('zlib') && $compressionLevel) {
68 $this->createGzipped = TRUE;
69 // $compressionLevel can also be TRUE
70 if (\TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($compressionLevel)) {
71 $this->gzipCompressionLevel = (int)$compressionLevel;
72 }
73 }
74 $this->setInitialPaths();
75 }
76
77 /**
78 * Sets initial values for paths.
79 *
80 * @return void
81 */
82 public function setInitialPaths() {
83 $this->setInitialRelativePath();
84 $this->setInitialRootPath();
85 $this->setInitialBackPath();
86 }
87
88 /**
89 * Sets relative back path
90 *
91 * @return void
92 */
93 protected function setInitialBackPath() {
94 $backPath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] : '';
95 $this->setBackPath($backPath);
96 }
97
98 /**
99 * Sets absolute path to working directory
100 *
101 * @return void
102 */
103 protected function setInitialRootPath() {
104 $rootPath = TYPO3_MODE === 'BE' ? PATH_typo3 : PATH_site;
105 $this->setRootPath($rootPath);
106 }
107
108 /**
109 * Sets relative path to PATH_site
110 *
111 * @return void
112 */
113 protected function setInitialRelativePath() {
114 $relativePath = TYPO3_MODE === 'BE' ? $GLOBALS['BACK_PATH'] . '../' : '';
115 $this->setRelativePath($relativePath);
116 }
117
118 /**
119 * Sets relative path to PATH_site
120 *
121 * @param string $relativePath Relative path to site root
122 * @return void
123 */
124 public function setRelativePath($relativePath) {
125 if (is_string($relativePath)) {
126 $this->relativePath = $relativePath;
127 }
128 }
129
130 /**
131 * Sets absolute path to working directory
132 *
133 * @param string $rootPath Absolute path
134 * @return void
135 */
136 public function setRootPath($rootPath) {
137 if (is_string($rootPath)) {
138 $this->rootPath = $rootPath;
139 }
140 }
141
142 /**
143 * Sets relative back path
144 *
145 * @param string $backPath Back path
146 * @return void
147 */
148 public function setBackPath($backPath) {
149 if (is_string($backPath)) {
150 $this->backPath = $backPath;
151 }
152 }
153
154 /**
155 * Concatenates the Stylesheet files
156 *
157 * Options:
158 * baseDirectories If set, only include files below one of the base directories
159 *
160 * @param array $cssFiles CSS files to process
161 * @param array $options Additional options
162 * @return array CSS files
163 */
164 public function concatenateCssFiles(array $cssFiles, array $options = array()) {
165 $filesToInclude = array();
166 foreach ($cssFiles as $key => $fileOptions) {
167 // no concatenation allowed for this file, so continue
168 if (!empty($fileOptions['excludeFromConcatenation'])) {
169 continue;
170 }
171 // we remove BACK_PATH from $filename, so make it relative to root path
172 $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
173 // if $options['baseDirectories'] set, we only include files below these directories
174 if ((!isset($options['baseDirectories']) || $this->checkBaseDirectory($filenameFromMainDir, array_merge($options['baseDirectories'], array($this->targetDirectory)))) && $fileOptions['media'] === 'all') {
175 if ($fileOptions['forceOnTop']) {
176 array_unshift($filesToInclude, $filenameFromMainDir);
177 } else {
178 $filesToInclude[] = $filenameFromMainDir;
179 }
180 // remove the file from the incoming file array
181 unset($cssFiles[$key]);
182 }
183 }
184 if (!empty($filesToInclude)) {
185 $targetFile = $this->createMergedCssFile($filesToInclude);
186 $targetFileRelative = $this->relativePath . $targetFile;
187 $concatenatedOptions = array(
188 'file' => $targetFileRelative,
189 'rel' => 'stylesheet',
190 'media' => 'all',
191 'compress' => TRUE,
192 'excludeFromConcatenation' => TRUE,
193 'forceOnTop' => FALSE,
194 'allWrap' => ''
195 );
196 // place the merged stylesheet on top of the stylesheets
197 $cssFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $cssFiles);
198 }
199 return $cssFiles;
200 }
201
202 /**
203 * Concatenates the JavaScript files
204 *
205 * @param array $jsFiles JavaScript files to process
206 * @return array JS files
207 */
208 public function concatenateJsFiles(array $jsFiles) {
209 $filesToInclude = array();
210 foreach ($jsFiles as $key => $fileOptions) {
211 // invalid section found or no concatenation allowed, so continue
212 if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
213 continue;
214 }
215 if (!isset($filesToInclude[$fileOptions['section']])) {
216 $filesToInclude[$fileOptions['section']] = array();
217 }
218 // we remove BACK_PATH from $filename, so make it relative to root path
219 $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
220 if ($fileOptions['forceOnTop']) {
221 array_unshift($filesToInclude[$fileOptions['section']], $filenameFromMainDir);
222 } else {
223 $filesToInclude[$fileOptions['section']][] = $filenameFromMainDir;
224 }
225 // remove the file from the incoming file array
226 unset($jsFiles[$key]);
227 }
228 if (!empty($filesToInclude)) {
229 foreach ($filesToInclude as $section => $files) {
230 $targetFile = $this->createMergedJsFile($files);
231 $targetFileRelative = $this->relativePath . $targetFile;
232 $concatenatedOptions = array(
233 'file' => $targetFileRelative,
234 'type' => 'text/javascript',
235 'section' => $section,
236 'compress' => TRUE,
237 'excludeFromConcatenation' => TRUE,
238 'forceOnTop' => FALSE,
239 'allWrap' => ''
240 );
241 // place the merged javascript on top of the JS files
242 $jsFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $jsFiles);
243 }
244 }
245 return $jsFiles;
246 }
247
248 /**
249 * Creates a merged CSS file
250 *
251 * @param array $filesToInclude Files which should be merged, paths relative to root path
252 * @return mixed Filename of the merged file
253 */
254 protected function createMergedCssFile(array $filesToInclude) {
255 return $this->createMergedFile($filesToInclude, 'css');
256 }
257
258 /**
259 * Creates a merged JS file
260 *
261 * @param array $filesToInclude Files which should be merged, paths relative to root path
262 * @return mixed Filename of the merged file
263 */
264 protected function createMergedJsFile(array $filesToInclude) {
265 return $this->createMergedFile($filesToInclude, 'js');
266 }
267
268 /**
269 * Creates a merged file with given file type
270 *
271 * @param array $filesToInclude Files which should be merged, paths relative to root path
272 * @param string $type File type
273 *
274 * @throws \InvalidArgumentException
275 * @return mixed Filename of the merged file
276 */
277 protected function createMergedFile(array $filesToInclude, $type = 'css') {
278 // Get file type
279 $type = strtolower(trim($type, '. '));
280 if (empty($type)) {
281 throw new \InvalidArgumentException('No valid file type given for files to be merged.', 1308957498);
282 }
283 // we add up the filenames, filemtimes and filsizes to later build a checksum over
284 // it and include it in the temporary file name
285 $unique = '';
286 foreach ($filesToInclude as $key => $filename) {
287 if (GeneralUtility::isValidUrl($filename)) {
288 // check if it is possibly a local file with fully qualified URL
289 if (GeneralUtility::isOnCurrentHost($filename) &&
290 GeneralUtility::isFirstPartOfStr(
291 $filename,
292 GeneralUtility::getIndpEnv('TYPO3_SITE_URL')
293 )
294 ) {
295 // attempt to turn it into a local file path
296 $localFilename = substr($filename, strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_URL')));
297 if (@is_file(GeneralUtility::resolveBackPath($this->rootPath . $localFilename))) {
298 $filesToInclude[$key] = $localFilename;
299 } else {
300 $filesToInclude[$key] = $this->retrieveExternalFile($filename);
301 }
302 } else {
303 $filesToInclude[$key] = $this->retrieveExternalFile($filename);
304 }
305 $filename = $filesToInclude[$key];
306 }
307 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $filename);
308 if (@file_exists($filenameAbsolute)) {
309 $fileStatus = stat($filenameAbsolute);
310 $unique .= $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
311 } else {
312 $unique .= $filenameAbsolute;
313 }
314 }
315 $targetFile = $this->targetDirectory . 'merged-' . md5($unique) . '.' . $type;
316 // if the file doesn't already exist, we create it
317 if (!file_exists((PATH_site . $targetFile))) {
318 $concatenated = '';
319 // concatenate all the files together
320 foreach ($filesToInclude as $filename) {
321 $contents = GeneralUtility::getUrl(GeneralUtility::resolveBackPath($this->rootPath . $filename));
322 // only fix paths if files aren't already in typo3temp (already processed)
323 if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
324 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
325 }
326 $concatenated .= LF . $contents;
327 }
328 // move @charset, @import and @namespace statements to top of new file
329 if ($type === 'css') {
330 $concatenated = $this->cssFixStatements($concatenated);
331 }
332 GeneralUtility::writeFile(PATH_site . $targetFile, $concatenated);
333 }
334 return $targetFile;
335 }
336
337 /**
338 * Compress multiple css files
339 *
340 * @param array $cssFiles The files to compress (array key = filename), relative to requested page
341 * @return array The CSS files after compression (array key = new filename), relative to requested page
342 */
343 public function compressCssFiles(array $cssFiles) {
344 $filesAfterCompression = array();
345 foreach ($cssFiles as $key => $fileOptions) {
346 // if compression is enabled
347 if ($fileOptions['compress']) {
348 $filename = $this->compressCssFile($fileOptions['file']);
349 $fileOptions['compress'] = FALSE;
350 $fileOptions['file'] = $filename;
351 $filesAfterCompression[$filename] = $fileOptions;
352 } else {
353 $filesAfterCompression[$key] = $fileOptions;
354 }
355 }
356 return $filesAfterCompression;
357 }
358
359 /**
360 * Compresses a CSS file
361 *
362 * Options:
363 * baseDirectories If set, only include files below one of the base directories
364 *
365 * removes comments and whitespaces
366 * Adopted from http://drupal.org/files/issues/minify_css.php__1.txt
367 *
368 * @param string $filename Source filename, relative to requested page
369 * @return string Compressed filename, relative to requested page
370 */
371 public function compressCssFile($filename) {
372 // generate the unique name of the file
373 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
374 if (@file_exists($filenameAbsolute)) {
375 $fileStatus = stat($filenameAbsolute);
376 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
377 } else {
378 $unique = $filenameAbsolute;
379 }
380
381 $pathinfo = PathUtility::pathinfo($filename);
382 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css';
383 // only create it, if it doesn't exist, yet
384 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
385 $contents = GeneralUtility::getUrl($filenameAbsolute);
386 // Compress CSS Content
387 $contents = $this->compressCssString($contents);
388 // Ensure file ends in newline.
389 // we have to fix relative paths, if we aren't working on a file in our target directory
390 if (strpos($filename, $this->targetDirectory) === FALSE) {
391 $filenameRelativeToMainDir = substr($filename, strlen($this->backPath));
392 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filenameRelativeToMainDir) . '/');
393 }
394 $this->writeFileAndCompressed($targetFile, $contents);
395 }
396 return $this->relativePath . $this->returnFileReference($targetFile);
397 }
398
399 /**
400 * Callback function for preg_replace
401 *
402 * @see compressCssFile
403 * @param array $matches
404 * @return string the compressed string
405 */
406 static public function compressCssPregCallback($matches) {
407 if ($matches[1]) {
408 // Group 1: Double quoted string.
409 return $matches[1];
410 } elseif ($matches[2]) {
411 // Group 2: Single quoted string.
412 return $matches[2];
413 } elseif ($matches[3]) {
414 // Group 3: Regular non-MacIE5-hack comment.
415 return '
416 ';
417 } elseif ($matches[4]) {
418 // Group 4: MacIE5-hack-type-1 comment.
419 return '
420 /*\\T1*/
421 ';
422 } elseif ($matches[5]) {
423 // Group 5,6,7: MacIE5-hack-type-2 comment
424 $matches[6] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[6]);
425 // Clean pre-punctuation.
426 $matches[6] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[6]);
427 // Clean post-punctuation.
428 $matches[6] = preg_replace('/;?\\}/S', '}
429 ', $matches[6]);
430 // Add a touch of formatting.
431 return '
432 /*T2\\*/' . $matches[6] . '
433 /*T2E*/
434 ';
435 } elseif ($matches[8]) {
436 // Group 8: calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
437 return 'calc' . $matches[8];
438 } elseif (isset($matches[9])) {
439 // Group 9: Non-string, non-comment. Safe to clean whitespace here.
440 $matches[9] = preg_replace('/^\\s++/', '', $matches[9]);
441 // Strip all leading whitespace.
442 $matches[9] = preg_replace('/\\s++$/', '', $matches[9]);
443 // Strip all trailing whitespace.
444 $matches[9] = preg_replace('/\\s{2,}+/', ' ', $matches[9]);
445 // Consolidate multiple whitespace.
446 $matches[9] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[9]);
447 // Clean pre-punctuation.
448 $matches[9] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[9]);
449 // Clean post-punctuation.
450 $matches[9] = preg_replace('/;?\\}/S', '}
451 ', $matches[9]);
452 // Add a touch of formatting.
453 return $matches[9];
454 }
455 return $matches[0] . '
456 /* ERROR! Unexpected _proccess_css_minify() parameter */
457 ';
458 }
459
460 /**
461 * Compress multiple javascript files
462 *
463 * @param array $jsFiles The files to compress (array key = filename), relative to requested page
464 * @return array The js files after compression (array key = new filename), relative to requested page
465 */
466 public function compressJsFiles(array $jsFiles) {
467 $filesAfterCompression = array();
468 foreach ($jsFiles as $fileName => $fileOptions) {
469 // If compression is enabled
470 if ($fileOptions['compress']) {
471 $compressedFilename = $this->compressJsFile($fileOptions['file']);
472 $fileOptions['compress'] = FALSE;
473 $fileOptions['file'] = $compressedFilename;
474 $filesAfterCompression[$compressedFilename] = $fileOptions;
475 } else {
476 $filesAfterCompression[$fileName] = $fileOptions;
477 }
478 }
479 return $filesAfterCompression;
480 }
481
482 /**
483 * Compresses a javascript file
484 *
485 * @param string $filename Source filename, relative to requested page
486 * @return string Filename of the compressed file, relative to requested page
487 */
488 public function compressJsFile($filename) {
489 // generate the unique name of the file
490 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
491 if (@file_exists($filenameAbsolute)) {
492 $fileStatus = stat($filenameAbsolute);
493 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
494 } else {
495 $unique = $filenameAbsolute;
496 }
497 $pathinfo = PathUtility::pathinfo($filename);
498 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
499 // only create it, if it doesn't exist, yet
500 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
501 $contents = GeneralUtility::getUrl($filenameAbsolute);
502 $this->writeFileAndCompressed($targetFile, $contents);
503 }
504 return $this->relativePath . $this->returnFileReference($targetFile);
505 }
506
507 /**
508 * Finds the relative path to a file, relative to the root path.
509 *
510 * @param string $filename the name of the file
511 * @return string the path to the file relative to the root path
512 */
513 protected function getFilenameFromMainDir($filename) {
514 // if BACK_PATH is empty return $filename
515 if (empty($this->backPath)) {
516 return $filename;
517 }
518 // if the file exists in the root path, just return the $filename
519 if (strpos($filename, $this->backPath) === 0) {
520 $file = str_replace($this->backPath, '', $filename);
521 if (is_file(GeneralUtility::resolveBackPath($this->rootPath . $file))) {
522 return $file;
523 }
524 }
525 // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
526 if (is_file(realpath(PATH_site . TYPO3_mainDir . $filename))) {
527 $filename = TYPO3_mainDir . $filename;
528 }
529 // build the file path relatively to the PATH_site
530 $backPath = str_replace(TYPO3_mainDir, '', $this->backPath);
531 $file = str_replace($backPath, '', $filename);
532 if (substr($file, 0, 3) === '../') {
533 $file = GeneralUtility::resolveBackPath(PATH_typo3 . $file);
534 } else {
535 $file = PATH_site . $file;
536 }
537 // check if the file exists, and if so, return the path relative to TYPO3_mainDir
538 if (is_file($file)) {
539 $mainDirDepth = substr_count(TYPO3_mainDir, '/');
540 return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file);
541 }
542 // none of above conditions were met, fallback to default behaviour
543 return substr($filename, strlen($this->backPath));
544 }
545
546 /**
547 * Decides whether a file comes from one of the baseDirectories
548 *
549 * @param string $filename Filename
550 * @param array $baseDirectories Base directories
551 * @return bool File belongs to a base directory or not
552 */
553 protected function checkBaseDirectory($filename, array $baseDirectories) {
554 foreach ($baseDirectories as $baseDirectory) {
555 // check, if $filename starts with base directory
556 if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
557 return TRUE;
558 }
559 }
560 return FALSE;
561 }
562
563 /**
564 * Fixes the relative paths inside of url() references in CSS files
565 *
566 * @param string $contents Data to process
567 * @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
568 * @return string Processed data
569 */
570 protected function cssFixRelativeUrlPaths($contents, $oldDir) {
571 $mainDir = TYPO3_MODE === 'BE' ? TYPO3_mainDir : '';
572 $newDir = '../../' . $mainDir . $oldDir;
573 // Replace "url()" paths
574 if (stripos($contents, 'url') !== FALSE) {
575 $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
576 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
577 }
578 // Replace "@import" paths
579 if (stripos($contents, '@import') !== FALSE) {
580 $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
581 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
582 }
583 return $contents;
584 }
585
586 /**
587 * Finds and replaces all URLs by using a given regex
588 *
589 * @param string $contents Data to process
590 * @param string $regex Regex used to find URLs in content
591 * @param string $newDir Path to prepend to the original file
592 * @param string $wrap Wrap around replaced values
593 * @return string Processed data
594 */
595 protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|') {
596 $matches = array();
597 $replacements = array();
598 $wrap = explode('|', $wrap);
599 preg_match_all($regex, $contents, $matches);
600 foreach ($matches[2] as $matchCount => $match) {
601 // remove '," or white-spaces around
602 $match = trim($match, '\'" ');
603 // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
604 if (strpos($match, ':') === FALSE && !preg_match('/url\\s*\\(/i', $match)) {
605 $newPath = GeneralUtility::resolveBackPath($newDir . $match);
606 $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
607 }
608 }
609 // replace URL paths in content
610 if (!empty($replacements)) {
611 $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
612 }
613 return $contents;
614 }
615
616 /**
617 * Moves @charset, @import and @namespace statements to the top of
618 * the content, because they must occur before all other CSS rules
619 *
620 * @param string $contents Data to process
621 * @return string Processed data
622 */
623 protected function cssFixStatements($contents) {
624 $matches = array();
625 $comment = LF . '/* moved by compressor */' . LF;
626 // nothing to do, so just return contents
627 if (stripos($contents, '@charset') === FALSE && stripos($contents, '@import') === FALSE && stripos($contents, '@namespace') === FALSE) {
628 return $contents;
629 }
630 $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\'\\)]+["\']?\\s*\\)?\\s*;/i';
631 preg_match_all($regex, $contents, $matches);
632 if (!empty($matches[0])) {
633 // remove existing statements
634 $contents = str_replace($matches[0], '', $contents);
635 // add statements to the top of contents in the order they occur in original file
636 $contents = $comment . implode($comment, $matches[0]) . LF . trim($contents);
637 }
638 return $contents;
639 }
640
641 /**
642 * Writes $contents into file $filename together with a gzipped version into $filename.gz
643 *
644 * @param string $filename Target filename
645 * @param string $contents File contents
646 * @return void
647 */
648 protected function writeFileAndCompressed($filename, $contents) {
649 // write uncompressed file
650 GeneralUtility::writeFile(PATH_site . $filename, $contents);
651 if ($this->createGzipped) {
652 // create compressed version
653 GeneralUtility::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
654 }
655 }
656
657 /**
658 * Decides whether a client can deal with gzipped content or not and returns the according file name,
659 * based on HTTP_ACCEPT_ENCODING
660 *
661 * @param string $filename File name
662 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING
663 */
664 protected function returnFileReference($filename) {
665 // if the client accepts gzip and we can create gzipped files, we give him compressed versions
666 if ($this->createGzipped && strpos(GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) {
667 return $filename . '.gzip';
668 } else {
669 return $filename;
670 }
671 }
672
673 /**
674 * Retrieves an external file and stores it locally.
675 *
676 * @param string $url
677 * @return string Temporary local filename for the externally-retrieved file
678 */
679 protected function retrieveExternalFile($url) {
680 $externalContent = GeneralUtility::getUrl($url);
681 $filename = $this->targetDirectory . 'external-' . md5($url);
682 // write only if file does not exist and md5 of the content is not the same as fetched one
683 if (!file_exists(PATH_site . $filename) &&
684 (md5($externalContent) !== md5(GeneralUtility::getUrl(PATH_site . $filename)))
685 ) {
686 GeneralUtility::writeFile(PATH_site . $filename, $externalContent);
687 }
688 return $filename;
689 }
690
691 /**
692 * Compress a CSS string by removing comments and whitespace characters
693 *
694 * @param string $contents
695 * @return string
696 */
697 protected function compressCssString($contents) {
698 // Perform some safe CSS optimizations.
699 $contents = str_replace(CR, '', $contents);
700 // Strip any and all carriage returns.
701 // Match and process strings, comments and everything else, one chunk at a time.
702 // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
703 $contents = preg_replace_callback('%
704 # One-regex-to-rule-them-all! - version: 20100220_0100
705 # Group 1: Match a double quoted string.
706 ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") | # or...
707 # Group 2: Match a single quoted string.
708 (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') | # or...
709 # Group 3: Match a regular non-MacIE5-hack comment.
710 (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) | # or...
711 # Group 4: Match a MacIE5-type1 comment.
712 (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) | # or...
713 # Group 5: Match a MacIE5-type2 comment.
714 (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/)) # folllowed by...
715 # Group 6: Match everything up to final closing regular comment
716 ([^/]*+(?:(?!\\*)/[^/]*+)*?)
717 # Group 7: Match final closing regular comment
718 (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) | # or...
719 # Group 8: Match a calc function (see http://www.w3.org/TR/2006/WD-css3-values-20060919/#calc)
720 (?:calc(\\((?:(?:[^\\(\\)]+)|(?8))*+\\))) | # or...
721 # Group 9: Match regular non-string, non-comment text.
722 ((?:[^"\'/](?!calc))*+(?:(?!/\\*)/(?:[^"\'/](?!calc))*+)*+)
723 %Ssx', array('self', 'compressCssPregCallback'), $contents);
724 // Do it!
725 $contents = preg_replace('/^\\s++/', '', $contents);
726 // Strip leading whitespace.
727 $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
728 ', $contents);
729 // Consolidate multi-lines space.
730 $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
731 ', $contents);
732
733 return $contents;
734 }
735
736 }