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