65294aa73884b9c1bf0f1a02ea5f86efe03454f0
[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 $key => $fileOptions) {
483 // if compression is enabled
484 if ($fileOptions['compress']) {
485 $fileOptions['file'] = $this->compressJsFile($fileOptions['file']);
486 }
487 $filesAfterCompression[$key] = $fileOptions;
488 }
489 return $filesAfterCompression;
490 }
491
492 /**
493 * Compresses a javascript file
494 *
495 * @param string $filename Source filename, relative to requested page
496 * @return string Filename of the compressed file, relative to requested page
497 */
498 public function compressJsFile($filename) {
499 // generate the unique name of the file
500 $filenameAbsolute = \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
501 $unique = $filenameAbsolute . filemtime($filenameAbsolute) . filesize($filenameAbsolute);
502 $pathinfo = PathUtility::pathinfo($filename);
503 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
504 // only create it, if it doesn't exist, yet
505 if (!file_exists((PATH_site . $targetFile)) || $this->createGzipped && !file_exists((PATH_site . $targetFile . '.gzip'))) {
506 $contents = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($filenameAbsolute);
507 $this->writeFileAndCompressed($targetFile, $contents);
508 }
509 return $this->relativePath . $this->returnFileReference($targetFile);
510 }
511
512 /**
513 * Finds the relative path to a file, relative to the root path.
514 *
515 * @param string $filename the name of the file
516 * @return string the path to the file relative to the root path
517 */
518 protected function getFilenameFromMainDir($filename) {
519 // if BACK_PATH is empty return $filename
520 if (empty($this->backPath)) {
521 return $filename;
522 }
523 // if the file exists in the root path, just return the $filename
524 if (strpos($filename, $this->backPath) === 0) {
525 $file = str_replace($this->backPath, '', $filename);
526 if (is_file($this->rootPath . $file)) {
527 return $file;
528 }
529 }
530 // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
531 if (is_file(realpath(PATH_site . TYPO3_mainDir . $filename))) {
532 $filename = TYPO3_mainDir . $filename;
533 }
534 // build the file path relatively to the PATH_site
535 $backPath = str_replace(TYPO3_mainDir, '', $this->backPath);
536 $file = str_replace($backPath, '', $filename);
537 if (substr($file, 0, 3) === '../') {
538 $file = \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath(PATH_typo3 . $file);
539 } else {
540 $file = PATH_site . $file;
541 }
542 // check if the file exists, and if so, return the path relative to TYPO3_mainDir
543 if (is_file($file)) {
544 $mainDirDepth = substr_count(TYPO3_mainDir, '/');
545 return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file);
546 }
547 // none of above conditions were met, fallback to default behaviour
548 return substr($filename, strlen($this->backPath));
549 }
550
551 /**
552 * Decides whether a file comes from one of the baseDirectories
553 *
554 * @param string $filename Filename
555 * @param array $baseDirectories Base directories
556 * @return boolean File belongs to a base directory or not
557 */
558 protected function checkBaseDirectory($filename, array $baseDirectories) {
559 foreach ($baseDirectories as $baseDirectory) {
560 // check, if $filename starts with base directory
561 if (\TYPO3\CMS\Core\Utility\GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
562 return TRUE;
563 }
564 }
565 return FALSE;
566 }
567
568 /**
569 * Fixes the relative paths inside of url() references in CSS files
570 *
571 * @param string $contents Data to process
572 * @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
573 * @return string Processed data
574 */
575 protected function cssFixRelativeUrlPaths($contents, $oldDir) {
576 $mainDir = TYPO3_MODE === 'BE' ? TYPO3_mainDir : '';
577 $newDir = '../../' . $mainDir . $oldDir;
578 // Replace "url()" paths
579 if (stripos($contents, 'url') !== FALSE) {
580 $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
581 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
582 }
583 // Replace "@import" paths
584 if (stripos($contents, '@import') !== FALSE) {
585 $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
586 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
587 }
588 return $contents;
589 }
590
591 /**
592 * Finds and replaces all URLs by using a given regex
593 *
594 * @param string $contents Data to process
595 * @param string $regex Regex used to find URLs in content
596 * @param string $newDir Path to prepend to the original file
597 * @param string $wrap Wrap around replaced values
598 * @return string Processed data
599 */
600 protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|') {
601 $matches = array();
602 $replacements = array();
603 $wrap = explode('|', $wrap);
604 preg_match_all($regex, $contents, $matches);
605 foreach ($matches[2] as $matchCount => $match) {
606 // remove '," or white-spaces around
607 $match = trim($match, '\'" ');
608 // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
609 if (strpos($match, ':') === FALSE && !preg_match('/url\\s*\\(/i', $match)) {
610 $newPath = \TYPO3\CMS\Core\Utility\GeneralUtility::resolveBackPath($newDir . $match);
611 $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
612 }
613 }
614 // replace URL paths in content
615 if (!empty($replacements)) {
616 $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
617 }
618 return $contents;
619 }
620
621 /**
622 * Moves @charset, @import and @namespace statements to the top of
623 * the content, because they must occur before all other CSS rules
624 *
625 * @param string $contents Data to process
626 * @return string Processed data
627 */
628 protected function cssFixStatements($contents) {
629 $matches = array();
630 $comment = LF . '/* moved by compressor */' . LF;
631 // nothing to do, so just return contents
632 if (stripos($contents, '@charset') === FALSE && stripos($contents, '@import') === FALSE && stripos($contents, '@namespace') === FALSE) {
633 return $contents;
634 }
635 $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\']+["\']?\\s*\\)?.*;/i';
636 preg_match_all($regex, $contents, $matches);
637 if (!empty($matches[0])) {
638 // remove existing statements
639 $contents = str_replace($matches[0], '', $contents);
640 // add statements to the top of contents in the order they occur in original file
641 $contents = $comment . implode($comment, $matches[0]) . LF . $contents;
642 }
643 return $contents;
644 }
645
646 /**
647 * Writes $contents into file $filename together with a gzipped version into $filename.gz
648 *
649 * @param string $filename Target filename
650 * @param string $contents File contents
651 * @return void
652 */
653 protected function writeFileAndCompressed($filename, $contents) {
654 // write uncompressed file
655 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile(PATH_site . $filename, $contents);
656 if ($this->createGzipped) {
657 // create compressed version
658 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
659 }
660 }
661
662 /**
663 * Decides whether a client can deal with gzipped content or not and returns the according file name,
664 * based on HTTP_ACCEPT_ENCODING
665 *
666 * @param string $filename File name
667 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING
668 */
669 protected function returnFileReference($filename) {
670 // if the client accepts gzip and we can create gzipped files, we give him compressed versions
671 if ($this->createGzipped && strpos(\TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) {
672 return $filename . '.gzip';
673 } else {
674 return $filename;
675 }
676 }
677
678 /**
679 * Retrieves an external file and stores it locally.
680 *
681 * @param string $url
682 * @return string Temporary local filename for the externally-retrieved file
683 */
684 protected function retrieveExternalFile($url) {
685 $externalContent = \TYPO3\CMS\Core\Utility\GeneralUtility::getUrl($url);
686 $filename = $this->targetDirectory . 'external-' . md5($url);
687 // write only if file does not exist and md5 of the content is not the same as fetched one
688 if (!file_exists(PATH_site . $filename) &&
689 (md5($externalContent) !== md5(\TYPO3\CMS\Core\Utility\GeneralUtility::getUrl(PATH_site . $filename)))
690 ) {
691 \TYPO3\CMS\Core\Utility\GeneralUtility::writeFile(PATH_site . $filename, $externalContent);
692 }
693 return $filename;
694 }
695
696 }
697
698
699 ?>