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