[BUGFIX] Compressor resolves dots in filenames correctly
[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 textfile 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 = intval($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 $filesToInclude[] = $filenameFromMainDir;
192 // remove the file from the incoming file array
193 unset($cssFiles[$key]);
194 }
195 }
196 if (count($filesToInclude)) {
197 $targetFile = $this->createMergedCssFile($filesToInclude);
198 $targetFileRelative = $this->relativePath . $targetFile;
199 $concatenatedOptions = array(
200 'file' => $targetFileRelative,
201 'rel' => 'stylesheet',
202 'media' => 'all',
203 'compress' => TRUE
204 );
205 // place the merged stylesheet on top of the stylesheets
206 $cssFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $cssFiles);
207 }
208 return $cssFiles;
209 }
210
211 /**
212 * Concatenates the JavaScript files
213 *
214 * @param array $jsFiles JavaScript files to process
215 * @return array JS files
216 */
217 public function concatenateJsFiles(array $jsFiles) {
218 $filesToInclude = array();
219 foreach ($jsFiles as $key => $fileOptions) {
220 // invalid section found or no concatenation allowed, so continue
221 if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
222 continue;
223 }
224 // we remove BACK_PATH from $filename, so make it relative to root path
225 $filesToInclude[$fileOptions['section']][] = $this->getFilenameFromMainDir($fileOptions['file']);
226 // remove the file from the incoming file array
227 unset($jsFiles[$key]);
228 }
229 if (!empty($filesToInclude)) {
230 foreach ($filesToInclude as $section => $files) {
231 $targetFile = $this->createMergedJsFile($files);
232 $targetFileRelative = $this->relativePath . $targetFile;
233 $concatenatedOptions = array(
234 'file' => $targetFileRelative,
235 'type' => 'text/javascript',
236 'section' => $section,
237 'compress' => 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('Error in TYPO3\\CMS\\Core\\Resource\\ResourceCompressor: No valid file type given for merged file', 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 $filepath = GeneralUtility::resolveBackPath($this->rootPath . $filename);
308 $unique .= $filename . filemtime($filepath) . filesize($filepath);
309 }
310 $targetFile = $this->targetDirectory . 'merged-' . md5($unique) . '.' . $type;
311 // if the file doesn't already exist, we create it
312 if (!file_exists((PATH_site . $targetFile))) {
313 $concatenated = '';
314 // concatenate all the files together
315 foreach ($filesToInclude as $filename) {
316 $contents = GeneralUtility::getUrl(GeneralUtility::resolveBackPath($this->rootPath . $filename));
317 // only fix paths if files aren't already in typo3temp (already processed)
318 if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
319 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
320 }
321 $concatenated .= LF . $contents;
322 }
323 // move @charset, @import and @namespace statements to top of new file
324 if ($type === 'css') {
325 $concatenated = $this->cssFixStatements($concatenated);
326 }
327 GeneralUtility::writeFile(PATH_site . $targetFile, $concatenated);
328 }
329 return $targetFile;
330 }
331
332 /**
333 * Compress multiple css files
334 *
335 * @param array $cssFiles The files to compress (array key = filename), relative to requested page
336 * @return array The CSS files after compression (array key = new filename), relative to requested page
337 */
338 public function compressCssFiles(array $cssFiles) {
339 $filesAfterCompression = array();
340 foreach ($cssFiles as $key => $fileOptions) {
341 // if compression is enabled
342 if ($fileOptions['compress']) {
343 $filename = $this->compressCssFile($fileOptions['file']);
344 $fileOptions['file'] = $filename;
345 $filesAfterCompression[$filename] = $fileOptions;
346 } else {
347 $filesAfterCompression[$key] = $fileOptions;
348 }
349 }
350 return $filesAfterCompression;
351 }
352
353 /**
354 * Compresses a CSS file
355 *
356 * Options:
357 * baseDirectories If set, only include files below one of the base directories
358 *
359 * removes comments and whitespaces
360 * Adopted from http://drupal.org/files/issues/minify_css.php__1.txt
361 *
362 * @param string $filename Source filename, relative to requested page
363 * @return string Compressed filename, relative to requested page
364 */
365 public function compressCssFile($filename) {
366 // generate the unique name of the file
367 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
368 $unique = $filenameAbsolute . filemtime($filenameAbsolute) . filesize($filenameAbsolute);
369 $pathinfo = PathUtility::pathinfo($filename);
370 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css';
371 // only create it, if it doesn't exist, yet
372 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
373 $contents = GeneralUtility::getUrl($filenameAbsolute);
374 // Perform some safe CSS optimizations.
375 $contents = str_replace(CR, '', $contents);
376 // Strip any and all carriage returns.
377 // Match and process strings, comments and everything else, one chunk at a time.
378 // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6.
379 $contents = preg_replace_callback('%
380 # One-regex-to-rule-them-all! - version: 20100220_0100
381 # Group 1: Match a double quoted string.
382 ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") | # or...
383 # Group 2: Match a single quoted string.
384 (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') | # or...
385 # Group 3: Match a regular non-MacIE5-hack comment.
386 (/\\*[^\\\\*]*+\\*++(?:[^\\\\*/][^\\\\*]*+\\*++)*+/) | # or...
387 # Group 4: Match a MacIE5-type1 comment.
388 (/\\*(?:[^*\\\\]*+\\**+(?!/))*+\\\\[^*]*+\\*++(?:[^*/][^*]*+\\*++)*+/(?<!\\\\\\*/)) | # or...
389 # Group 5: Match a MacIE5-type2 comment.
390 (/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/(?<=\\\\\\*/)) # folllowed by...
391 # Group 6: Match everything up to final closing regular comment
392 ([^/]*+(?:(?!\\*)/[^/]*+)*?)
393 # Group 7: Match final closing regular comment
394 (/\\*[^/]++(?:(?<!\\*)/(?!\\*)[^/]*+)*+/(?<=(?<!\\\\)\\*/)) | # or...
395 # Group 8: Match regular non-string, non-comment text.
396 ([^"\'/]*+(?:(?!/\\*)/[^"\'/]*+)*+)
397 %Ssx', array('self', 'compressCssPregCallback'), $contents);
398 // Do it!
399 $contents = preg_replace('/^\\s++/', '', $contents);
400 // Strip leading whitespace.
401 $contents = preg_replace('/[ \\t]*+\\n\\s*+/S', '
402 ', $contents);
403 // Consolidate multi-lines space.
404 $contents = preg_replace('/(?<!\\s)\\s*+$/S', '
405 ', $contents);
406 // Ensure file ends in newline.
407 // we have to fix relative paths, if we aren't working on a file in our target directory
408 if (strpos($filename, $this->targetDirectory) === FALSE) {
409 $filenameRelativeToMainDir = substr($filename, strlen($this->backPath));
410 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filenameRelativeToMainDir) . '/');
411 }
412 $this->writeFileAndCompressed($targetFile, $contents);
413 }
414 return $this->relativePath . $this->returnFileReference($targetFile);
415 }
416
417 /**
418 * Callback function for preg_replace
419 *
420 * @see compressCssFile
421 * @param array $matches
422 * @return string the compressed string
423 */
424 static public function compressCssPregCallback($matches) {
425 if ($matches[1]) {
426 // Group 1: Double quoted string.
427 return $matches[1];
428 } elseif ($matches[2]) {
429 // Group 2: Single quoted string.
430 return $matches[2];
431 } elseif ($matches[3]) {
432 // Group 3: Regular non-MacIE5-hack comment.
433 return '
434 ';
435 } elseif ($matches[4]) {
436 // Group 4: MacIE5-hack-type-1 comment.
437 return '
438 /*\\T1*/
439 ';
440 } elseif ($matches[5]) {
441 // Group 5,6,7: MacIE5-hack-type-2 comment
442 $matches[6] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[6]);
443 // Clean pre-punctuation.
444 $matches[6] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[6]);
445 // Clean post-punctuation.
446 $matches[6] = preg_replace('/;?\\}/S', '}
447 ', $matches[6]);
448 // Add a touch of formatting.
449 return '
450 /*T2\\*/' . $matches[6] . '
451 /*T2E*/
452 ';
453 } elseif (isset($matches[8])) {
454 // Group 8: Non-string, non-comment. Safe to clean whitespace here.
455 $matches[8] = preg_replace('/^\\s++/', '', $matches[8]);
456 // Strip all leading whitespace.
457 $matches[8] = preg_replace('/\\s++$/', '', $matches[8]);
458 // Strip all trailing whitespace.
459 $matches[8] = preg_replace('/\\s{2,}+/', ' ', $matches[8]);
460 // Consolidate multiple whitespace.
461 $matches[8] = preg_replace('/\\s++([+>{};,)])/S', '$1', $matches[8]);
462 // Clean pre-punctuation.
463 $matches[8] = preg_replace('/([+>{}:;,(])\\s++/S', '$1', $matches[8]);
464 // Clean post-punctuation.
465 $matches[8] = preg_replace('/;?\\}/S', '}
466 ', $matches[8]);
467 // Add a touch of formatting.
468 return $matches[8];
469 }
470 return $matches[0] . '
471 /* ERROR! Unexpected _proccess_css_minify() parameter */
472 ';
473 }
474
475 /**
476 * Compress multiple javascript files
477 *
478 * @param array $jsFiles The files to compress (array key = filename), relative to requested page
479 * @return array The js files after compression (array key = new filename), relative to requested page
480 */
481 public function compressJsFiles(array $jsFiles) {
482 $filesAfterCompression = array();
483 foreach ($jsFiles as $fileName => $fileOptions) {
484 // If compression is enabled
485 if ($fileOptions['compress']) {
486 $compressedFilename = $this->compressJsFile($fileOptions['file']);
487 $fileOptions['file'] = $compressedFilename;
488 $filesAfterCompression[$compressedFilename] = $fileOptions;
489 } else {
490 $filesAfterCompression[$fileName] = $fileOptions;
491 }
492 }
493 return $filesAfterCompression;
494 }
495
496 /**
497 * Compresses a javascript file
498 *
499 * @param string $filename Source filename, relative to requested page
500 * @return string Filename of the compressed file, relative to requested page
501 */
502 public function compressJsFile($filename) {
503 // generate the unique name of the file
504 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
505 $unique = $filenameAbsolute . filemtime($filenameAbsolute) . filesize($filenameAbsolute);
506 $pathinfo = PathUtility::pathinfo($filename);
507 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
508 // only create it, if it doesn't exist, yet
509 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
510 $contents = GeneralUtility::getUrl($filenameAbsolute);
511 $this->writeFileAndCompressed($targetFile, $contents);
512 }
513 return $this->relativePath . $this->returnFileReference($targetFile);
514 }
515
516 /**
517 * Finds the relative path to a file, relative to the root path.
518 *
519 * @param string $filename the name of the file
520 * @return string the path to the file relative to the root path
521 */
522 protected function getFilenameFromMainDir($filename) {
523 // if BACK_PATH is empty return $filename
524 if (empty($this->backPath)) {
525 return $filename;
526 }
527 // if the file exists in the root path, just return the $filename
528 if (strpos($filename, $this->backPath) === 0) {
529 $file = str_replace($this->backPath, '', $filename);
530 if (is_file(GeneralUtility::resolveBackPath($this->rootPath . $file))) {
531 return $file;
532 }
533 }
534 // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
535 if (is_file(realpath(PATH_site . TYPO3_mainDir . $filename))) {
536 $filename = TYPO3_mainDir . $filename;
537 }
538 // build the file path relatively to the PATH_site
539 $backPath = str_replace(TYPO3_mainDir, '', $this->backPath);
540 $file = str_replace($backPath, '', $filename);
541 if (substr($file, 0, 3) === '../') {
542 $file = GeneralUtility::resolveBackPath(PATH_typo3 . $file);
543 } else {
544 $file = PATH_site . $file;
545 }
546 // check if the file exists, and if so, return the path relative to TYPO3_mainDir
547 if (is_file($file)) {
548 $mainDirDepth = substr_count(TYPO3_mainDir, '/');
549 return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file);
550 }
551 // none of above conditions were met, fallback to default behaviour
552 return substr($filename, strlen($this->backPath));
553 }
554
555 /**
556 * Decides whether a file comes from one of the baseDirectories
557 *
558 * @param string $filename Filename
559 * @param array $baseDirectories Base directories
560 * @return boolean File belongs to a base directory or not
561 */
562 protected function checkBaseDirectory($filename, array $baseDirectories) {
563 foreach ($baseDirectories as $baseDirectory) {
564 // check, if $filename starts with base directory
565 if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
566 return TRUE;
567 }
568 }
569 return FALSE;
570 }
571
572 /**
573 * Fixes the relative paths inside of url() references in CSS files
574 *
575 * @param string $contents Data to process
576 * @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
577 * @return string Processed data
578 */
579 protected function cssFixRelativeUrlPaths($contents, $oldDir) {
580 $mainDir = TYPO3_MODE === 'BE' ? TYPO3_mainDir : '';
581 $newDir = '../../' . $mainDir . $oldDir;
582 // Replace "url()" paths
583 if (stripos($contents, 'url') !== FALSE) {
584 $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
585 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
586 }
587 // Replace "@import" paths
588 if (stripos($contents, '@import') !== FALSE) {
589 $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
590 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
591 }
592 return $contents;
593 }
594
595 /**
596 * Finds and replaces all URLs by using a given regex
597 *
598 * @param string $contents Data to process
599 * @param string $regex Regex used to find URLs in content
600 * @param string $newDir Path to prepend to the original file
601 * @param string $wrap Wrap around replaced values
602 * @return string Processed data
603 */
604 protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|') {
605 $matches = array();
606 $replacements = array();
607 $wrap = explode('|', $wrap);
608 preg_match_all($regex, $contents, $matches);
609 foreach ($matches[2] as $matchCount => $match) {
610 // remove '," or white-spaces around
611 $match = trim($match, '\'" ');
612 // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
613 if (strpos($match, ':') === FALSE && !preg_match('/url\\s*\\(/i', $match)) {
614 $newPath = GeneralUtility::resolveBackPath($newDir . $match);
615 $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
616 }
617 }
618 // replace URL paths in content
619 if (!empty($replacements)) {
620 $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
621 }
622 return $contents;
623 }
624
625 /**
626 * Moves @charset, @import and @namespace statements to the top of
627 * the content, because they must occur before all other CSS rules
628 *
629 * @param string $contents Data to process
630 * @return string Processed data
631 */
632 protected function cssFixStatements($contents) {
633 $matches = array();
634 $comment = LF . '/* moved by compressor */' . LF;
635 // nothing to do, so just return contents
636 if (stripos($contents, '@charset') === FALSE && stripos($contents, '@import') === FALSE && stripos($contents, '@namespace') === FALSE) {
637 return $contents;
638 }
639 $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\']+["\']?\\s*\\)?.*;/i';
640 preg_match_all($regex, $contents, $matches);
641 if (!empty($matches[0])) {
642 // remove existing statements
643 $contents = str_replace($matches[0], '', $contents);
644 // add statements to the top of contents in the order they occur in original file
645 $contents = $comment . implode($comment, $matches[0]) . LF . $contents;
646 }
647 return $contents;
648 }
649
650 /**
651 * Writes $contents into file $filename together with a gzipped version into $filename.gz
652 *
653 * @param string $filename Target filename
654 * @param string $contents File contents
655 * @return void
656 */
657 protected function writeFileAndCompressed($filename, $contents) {
658 // write uncompressed file
659 GeneralUtility::writeFile(PATH_site . $filename, $contents);
660 if ($this->createGzipped) {
661 // create compressed version
662 GeneralUtility::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
663 }
664 }
665
666 /**
667 * Decides whether a client can deal with gzipped content or not and returns the according file name,
668 * based on HTTP_ACCEPT_ENCODING
669 *
670 * @param string $filename File name
671 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING
672 */
673 protected function returnFileReference($filename) {
674 // if the client accepts gzip and we can create gzipped files, we give him compressed versions
675 if ($this->createGzipped && strpos(GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) {
676 return $filename . '.gzip';
677 } else {
678 return $filename;
679 }
680 }
681
682 /**
683 * Retrieves an external file and stores it locally.
684 *
685 * @param string $url
686 * @return string Temporary local filename for the externally-retrieved file
687 */
688 protected function retrieveExternalFile($url) {
689 $externalContent = GeneralUtility::getUrl($url);
690 $filename = $this->targetDirectory . 'external-' . md5($url);
691 // write only if file does not exist and md5 of the content is not the same as fetched one
692 if (!file_exists(PATH_site . $filename) &&
693 (md5($externalContent) !== md5(GeneralUtility::getUrl(PATH_site . $filename)))
694 ) {
695 GeneralUtility::writeFile(PATH_site . $filename, $externalContent);
696 }
697 return $filename;
698 }
699
700 }