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