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