[BUGFIX] Add initialization of DataMapper on QueryResult object wakeup
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ResourceCompressor.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Core\Environment;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Core\Utility\MathUtility;
20 use TYPO3\CMS\Core\Utility\PathUtility;
21
22 /**
23 * Compressor
24 * This merges and compresses CSS and JavaScript files of the TYPO3 Backend.
25 */
26 class ResourceCompressor
27 {
28 /**
29 * @var string
30 */
31 protected $targetDirectory = 'typo3temp/assets/compressed/';
32
33 /**
34 * @var string
35 */
36 protected $rootPath = '';
37
38 /**
39 * gzipped versions are only created if $TYPO3_CONF_VARS[TYPO3_MODE]['compressionLevel'] is set
40 *
41 * @var bool
42 */
43 protected $createGzipped = false;
44
45 /**
46 * @var int
47 */
48 protected $gzipCompressionLevel = -1;
49
50 /**
51 * @var string
52 */
53 protected $htaccessTemplate = '<FilesMatch "\\.(js|css)(\\.gzip)?$">
54 <IfModule mod_expires.c>
55 ExpiresActive on
56 ExpiresDefault "access plus 7 days"
57 </IfModule>
58 FileETag MTime Size
59 </FilesMatch>';
60
61 /**
62 * Constructor
63 */
64 public function __construct()
65 {
66 // we check for existence of our targetDirectory
67 if (!is_dir(Environment::getPublicPath() . '/' . $this->targetDirectory)) {
68 GeneralUtility::mkdir_deep(Environment::getPublicPath() . '/' . $this->targetDirectory);
69 }
70 // if enabled, we check whether we should auto-create the .htaccess file
71 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) {
72 // check whether .htaccess exists
73 $htaccessPath = Environment::getPublicPath() . '/' . $this->targetDirectory . '.htaccess';
74 if (!file_exists($htaccessPath)) {
75 GeneralUtility::writeFile($htaccessPath, $this->htaccessTemplate);
76 }
77 }
78 // decide whether we should create gzipped versions or not
79 $compressionLevel = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['compressionLevel'];
80 // we need zlib for gzencode()
81 if (extension_loaded('zlib') && $compressionLevel) {
82 $this->createGzipped = true;
83 // $compressionLevel can also be TRUE
84 if (MathUtility::canBeInterpretedAsInteger($compressionLevel)) {
85 $this->gzipCompressionLevel = (int)$compressionLevel;
86 }
87 }
88 $this->setRootPath(TYPO3_MODE === 'BE' ? Environment::getBackendPath() . '/' : Environment::getPublicPath() . '/');
89 }
90
91 /**
92 * Sets absolute path to working directory
93 *
94 * @param string $rootPath Absolute path
95 */
96 public function setRootPath($rootPath)
97 {
98 if (is_string($rootPath)) {
99 $this->rootPath = $rootPath;
100 }
101 }
102
103 /**
104 * Concatenates the Stylesheet files
105 *
106 * @param array $cssFiles CSS files to process
107 * @return array CSS files
108 */
109 public function concatenateCssFiles(array $cssFiles)
110 {
111 $filesToIncludeByType = ['all' => []];
112 foreach ($cssFiles as $key => $fileOptions) {
113 // no concatenation allowed for this file, so continue
114 if (!empty($fileOptions['excludeFromConcatenation'])) {
115 continue;
116 }
117 $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
118 $type = isset($fileOptions['media']) ? strtolower($fileOptions['media']) : 'all';
119 if (!isset($filesToIncludeByType[$type])) {
120 $filesToIncludeByType[$type] = [];
121 }
122 if (!empty($fileOptions['forceOnTop'])) {
123 array_unshift($filesToIncludeByType[$type], $filenameFromMainDir);
124 } else {
125 $filesToIncludeByType[$type][] = $filenameFromMainDir;
126 }
127 // remove the file from the incoming file array
128 unset($cssFiles[$key]);
129 }
130 foreach ($filesToIncludeByType as $mediaOption => $filesToInclude) {
131 if (empty($filesToInclude)) {
132 continue;
133 }
134 $targetFile = $this->createMergedCssFile($filesToInclude);
135 $concatenatedOptions = [
136 'file' => $targetFile,
137 'rel' => 'stylesheet',
138 'media' => $mediaOption,
139 'compress' => true,
140 'excludeFromConcatenation' => true,
141 'forceOnTop' => false,
142 'allWrap' => ''
143 ];
144 // place the merged stylesheet on top of the stylesheets
145 $cssFiles = array_merge($cssFiles, [$targetFile => $concatenatedOptions]);
146 }
147 return $cssFiles;
148 }
149
150 /**
151 * Concatenates the JavaScript files
152 *
153 * @param array $jsFiles JavaScript files to process
154 * @return array JS files
155 */
156 public function concatenateJsFiles(array $jsFiles)
157 {
158 $concatenatedJsFileIsAsync = false;
159 $allFilesToConcatenateAreAsync = true;
160 $filesToInclude = [];
161 foreach ($jsFiles as $key => $fileOptions) {
162 // invalid section found or no concatenation allowed, so continue
163 if (empty($fileOptions['section']) || !empty($fileOptions['excludeFromConcatenation'])) {
164 continue;
165 }
166 if (!isset($filesToInclude[$fileOptions['section']])) {
167 $filesToInclude[$fileOptions['section']] = [];
168 }
169 $filenameFromMainDir = $this->getFilenameFromMainDir($fileOptions['file']);
170 if (!empty($fileOptions['forceOnTop'])) {
171 array_unshift($filesToInclude[$fileOptions['section']], $filenameFromMainDir);
172 } else {
173 $filesToInclude[$fileOptions['section']][] = $filenameFromMainDir;
174 }
175 if (!empty($fileOptions['async']) && (bool)$fileOptions['async']) {
176 $concatenatedJsFileIsAsync = true;
177 } else {
178 $allFilesToConcatenateAreAsync = false;
179 }
180 // remove the file from the incoming file array
181 unset($jsFiles[$key]);
182 }
183 if (!empty($filesToInclude)) {
184 foreach ($filesToInclude as $section => $files) {
185 $targetFile = $this->createMergedJsFile($files);
186 $concatenatedOptions = [
187 'file' => $targetFile,
188 'type' => 'text/javascript',
189 'section' => $section,
190 'compress' => true,
191 'excludeFromConcatenation' => true,
192 'forceOnTop' => false,
193 'allWrap' => '',
194 'async' => $concatenatedJsFileIsAsync && $allFilesToConcatenateAreAsync,
195 ];
196 // place the merged javascript on top of the JS files
197 $jsFiles = array_merge([$targetFile => $concatenatedOptions], $jsFiles);
198 }
199 }
200 return $jsFiles;
201 }
202
203 /**
204 * Creates a merged CSS file
205 *
206 * @param array $filesToInclude Files which should be merged, paths relative to root path
207 * @return mixed Filename of the merged file
208 */
209 protected function createMergedCssFile(array $filesToInclude)
210 {
211 return $this->createMergedFile($filesToInclude, 'css');
212 }
213
214 /**
215 * Creates a merged JS file
216 *
217 * @param array $filesToInclude Files which should be merged, paths relative to root path
218 * @return mixed Filename of the merged file
219 */
220 protected function createMergedJsFile(array $filesToInclude)
221 {
222 return $this->createMergedFile($filesToInclude, 'js');
223 }
224
225 /**
226 * Creates a merged file with given file type
227 *
228 * @param array $filesToInclude Files which should be merged, paths relative to root path
229 * @param string $type File type
230 *
231 * @throws \InvalidArgumentException
232 * @return mixed Filename of the merged file
233 */
234 protected function createMergedFile(array $filesToInclude, $type = 'css')
235 {
236 // Get file type
237 $type = strtolower(trim($type, '. '));
238 if (empty($type)) {
239 throw new \InvalidArgumentException('No valid file type given for files to be merged.', 1308957498);
240 }
241 // we add up the filenames, filemtimes and filsizes to later build a checksum over
242 // it and include it in the temporary file name
243 $unique = '';
244 foreach ($filesToInclude as $key => $filename) {
245 if (GeneralUtility::isValidUrl($filename)) {
246 // check if it is possibly a local file with fully qualified URL
247 if (GeneralUtility::isOnCurrentHost($filename) &&
248 GeneralUtility::isFirstPartOfStr(
249 $filename,
250 GeneralUtility::getIndpEnv('TYPO3_SITE_URL')
251 )
252 ) {
253 // attempt to turn it into a local file path
254 $localFilename = substr($filename, strlen(GeneralUtility::getIndpEnv('TYPO3_SITE_URL')));
255 if (@is_file(GeneralUtility::resolveBackPath($this->rootPath . $localFilename))) {
256 $filesToInclude[$key] = $localFilename;
257 } else {
258 $filesToInclude[$key] = $this->retrieveExternalFile($filename);
259 }
260 } else {
261 $filesToInclude[$key] = $this->retrieveExternalFile($filename);
262 }
263 $filename = $filesToInclude[$key];
264 }
265 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $filename);
266 if (@file_exists($filenameAbsolute)) {
267 $fileStatus = stat($filenameAbsolute);
268 $unique .= $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
269 } else {
270 $unique .= $filenameAbsolute;
271 }
272 }
273 $targetFile = $this->targetDirectory . 'merged-' . md5($unique) . '.' . $type;
274 // if the file doesn't already exist, we create it
275 if (!file_exists(Environment::getPublicPath() . '/' . $targetFile)) {
276 $concatenated = '';
277 // concatenate all the files together
278 foreach ($filesToInclude as $filename) {
279 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $filename);
280 $filename = PathUtility::stripPathSitePrefix($filenameAbsolute);
281 $contents = file_get_contents($filenameAbsolute);
282 // remove any UTF-8 byte order mark (BOM) from files
283 if (strpos($contents, "\xEF\xBB\xBF") === 0) {
284 $contents = substr($contents, 3);
285 }
286 // only fix paths if files aren't already in typo3temp (already processed)
287 if ($type === 'css' && !GeneralUtility::isFirstPartOfStr($filename, $this->targetDirectory)) {
288 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
289 }
290 $concatenated .= LF . $contents;
291 }
292 // move @charset, @import and @namespace statements to top of new file
293 if ($type === 'css') {
294 $concatenated = $this->cssFixStatements($concatenated);
295 }
296 GeneralUtility::writeFile(Environment::getPublicPath() . '/' . $targetFile, $concatenated);
297 }
298 return $targetFile;
299 }
300
301 /**
302 * Compress multiple css files
303 *
304 * @param array $cssFiles The files to compress (array key = filename), relative to requested page
305 * @return array The CSS files after compression (array key = new filename), relative to requested page
306 */
307 public function compressCssFiles(array $cssFiles)
308 {
309 $filesAfterCompression = [];
310 foreach ($cssFiles as $key => $fileOptions) {
311 // if compression is enabled
312 if ($fileOptions['compress']) {
313 $filename = $this->compressCssFile($fileOptions['file']);
314 $fileOptions['compress'] = false;
315 $fileOptions['file'] = $filename;
316 $filesAfterCompression[$filename] = $fileOptions;
317 } else {
318 $filesAfterCompression[$key] = $fileOptions;
319 }
320 }
321 return $filesAfterCompression;
322 }
323
324 /**
325 * Compresses a CSS file
326 *
327 * Options:
328 * baseDirectories If set, only include files below one of the base directories
329 *
330 * removes comments and whitespaces
331 * Adopted from https://github.com/drupal/drupal/blob/8.0.x/core/lib/Drupal/Core/Asset/CssOptimizer.php
332 *
333 * @param string $filename Source filename, relative to requested page
334 * @return string Compressed filename, relative to requested page
335 */
336 public function compressCssFile($filename)
337 {
338 // generate the unique name of the file
339 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
340 if (@file_exists($filenameAbsolute)) {
341 $fileStatus = stat($filenameAbsolute);
342 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
343 } else {
344 $unique = $filenameAbsolute;
345 }
346 // make sure it is again the full filename
347 $filename = PathUtility::stripPathSitePrefix($filenameAbsolute);
348
349 $pathinfo = PathUtility::pathinfo($filenameAbsolute);
350 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css';
351 // only create it, if it doesn't exist, yet
352 if (!file_exists(Environment::getPublicPath() . '/' . $targetFile) || $this->createGzipped && !file_exists(Environment::getPublicPath() . '/' . $targetFile . '.gzip')) {
353 $contents = $this->compressCssString(file_get_contents($filenameAbsolute));
354 if (strpos($filename, $this->targetDirectory) === false) {
355 $contents = $this->cssFixRelativeUrlPaths($contents, PathUtility::dirname($filename) . '/');
356 }
357 $this->writeFileAndCompressed($targetFile, $contents);
358 }
359 return $this->returnFileReference($targetFile);
360 }
361
362 /**
363 * Compress multiple javascript files
364 *
365 * @param array $jsFiles The files to compress (array key = filename), relative to requested page
366 * @return array The js files after compression (array key = new filename), relative to requested page
367 */
368 public function compressJsFiles(array $jsFiles)
369 {
370 $filesAfterCompression = [];
371 foreach ($jsFiles as $fileName => $fileOptions) {
372 // If compression is enabled
373 if ($fileOptions['compress']) {
374 $compressedFilename = $this->compressJsFile($fileOptions['file']);
375 $fileOptions['compress'] = false;
376 $fileOptions['file'] = $compressedFilename;
377 $filesAfterCompression[$compressedFilename] = $fileOptions;
378 } else {
379 $filesAfterCompression[$fileName] = $fileOptions;
380 }
381 }
382 return $filesAfterCompression;
383 }
384
385 /**
386 * Compresses a javascript file
387 *
388 * @param string $filename Source filename, relative to requested page
389 * @return string Filename of the compressed file, relative to requested page
390 */
391 public function compressJsFile($filename)
392 {
393 // generate the unique name of the file
394 $filenameAbsolute = GeneralUtility::resolveBackPath($this->rootPath . $this->getFilenameFromMainDir($filename));
395 if (@file_exists($filenameAbsolute)) {
396 $fileStatus = stat($filenameAbsolute);
397 $unique = $filenameAbsolute . $fileStatus['mtime'] . $fileStatus['size'];
398 } else {
399 $unique = $filenameAbsolute;
400 }
401 $pathinfo = PathUtility::pathinfo($filename);
402 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js';
403 // only create it, if it doesn't exist, yet
404 if (!file_exists(Environment::getPublicPath() . '/' . $targetFile) || $this->createGzipped && !file_exists(Environment::getPublicPath() . '/' . $targetFile . '.gzip')) {
405 $contents = file_get_contents($filenameAbsolute);
406 $this->writeFileAndCompressed($targetFile, $contents);
407 }
408 return $this->returnFileReference($targetFile);
409 }
410
411 /**
412 * Finds the relative path to a file, relative to the root path.
413 *
414 * @param string $filename the name of the file
415 * @return string the path to the file relative to the root path ($this->rootPath)
416 */
417 protected function getFilenameFromMainDir($filename)
418 {
419 /*
420 * The various paths may have those values (e.g. if TYPO3 is installed in a subdir)
421 * - docRoot = /var/www/html/
422 * - Environment::getPublicPath() = /var/www/html/sites/site1/
423 * - $this->rootPath = /var/www/html/sites/site1/typo3
424 *
425 * The file names passed into this function may be either:
426 * - relative to $this->rootPath
427 * - relative to Environment::getPublicPath()
428 * - relative to docRoot
429 */
430 $docRoot = GeneralUtility::getIndpEnv('TYPO3_DOCUMENT_ROOT');
431 $fileNameWithoutSlash = ltrim($filename, '/');
432
433 // if the file is an absolute reference within the docRoot
434 $absolutePath = $docRoot . '/' . $fileNameWithoutSlash;
435 // if it is already an absolute path to the file
436 if (PathUtility::isAbsolutePath($filename)) {
437 $absolutePath = $filename;
438 }
439 // Calling is_file without @ for a path starting with '../' causes a PHP Warning when using open_basedir restriction
440 if (@is_file($absolutePath)) {
441 if (strpos($absolutePath, $this->rootPath) === 0) {
442 // the path is within the current root path, simply strip rootPath off
443 return substr($absolutePath, strlen($this->rootPath));
444 }
445 // the path is not within the root path, strip off the site path, the remaining logic below
446 // takes care about adjusting the path correctly.
447 $filename = substr($absolutePath, strlen(Environment::getPublicPath() . '/'));
448 }
449 // if the file exists in the root path, just return the $filename
450 if (is_file($this->rootPath . $fileNameWithoutSlash)) {
451 return $fileNameWithoutSlash;
452 }
453 // if the file is from a special TYPO3 internal directory, add the missing typo3/ prefix
454 if (is_file(realpath(Environment::getBackendPath() . '/' . $filename))) {
455 $filename = 'typo3/' . $filename;
456 }
457 // build the file path relative to the public web path
458 if (strpos($filename, 'EXT:') === 0) {
459 $file = GeneralUtility::getFileAbsFileName($filename);
460 } elseif (strpos($filename, '../') === 0) {
461 $file = GeneralUtility::resolveBackPath(Environment::getBackendPath() . '/' . $filename);
462 } else {
463 $file = Environment::getPublicPath() . '/' . $filename;
464 }
465
466 // check if the file exists, and if so, return the path relative to current PHP script
467 if (is_file($file)) {
468 return rtrim(PathUtility::getRelativePathTo($file), '/');
469 }
470 // none of above conditions were met, fallback to default behaviour
471 return $filename;
472 }
473
474 /**
475 * Decides whether a file comes from one of the baseDirectories
476 *
477 * @param string $filename Filename
478 * @param array $baseDirectories Base directories
479 * @return bool File belongs to a base directory or not
480 */
481 protected function checkBaseDirectory($filename, array $baseDirectories)
482 {
483 foreach ($baseDirectories as $baseDirectory) {
484 // check, if $filename starts with base directory
485 if (GeneralUtility::isFirstPartOfStr($filename, $baseDirectory)) {
486 return true;
487 }
488 }
489 return false;
490 }
491
492 /**
493 * Fixes the relative paths inside of url() references in CSS files
494 *
495 * @param string $contents Data to process
496 * @param string $oldDir Directory of the original file, relative to TYPO3_mainDir
497 * @return string Processed data
498 */
499 protected function cssFixRelativeUrlPaths($contents, $oldDir)
500 {
501 $newDir = '../../../' . $oldDir;
502 // Replace "url()" paths
503 if (stripos($contents, 'url') !== false) {
504 $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
505 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '(\'|\')');
506 }
507 // Replace "@import" paths
508 if (stripos($contents, '@import') !== false) {
509 $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
510 $contents = $this->findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, '"|"');
511 }
512 return $contents;
513 }
514
515 /**
516 * Finds and replaces all URLs by using a given regex
517 *
518 * @param string $contents Data to process
519 * @param string $regex Regex used to find URLs in content
520 * @param string $newDir Path to prepend to the original file
521 * @param string $wrap Wrap around replaced values
522 * @return string Processed data
523 */
524 protected function findAndReplaceUrlPathsByRegex($contents, $regex, $newDir, $wrap = '|')
525 {
526 $matches = [];
527 $replacements = [];
528 $wrap = explode('|', $wrap);
529 preg_match_all($regex, $contents, $matches);
530 foreach ($matches[2] as $matchCount => $match) {
531 // remove '," or white-spaces around
532 $match = trim($match, '\'" ');
533 // we must not rewrite paths containing ":" or "url(", e.g. data URIs (see RFC 2397)
534 if (strpos($match, ':') === false && !preg_match('/url\\s*\\(/i', $match)) {
535 $newPath = GeneralUtility::resolveBackPath($newDir . $match);
536 $replacements[$matches[1][$matchCount]] = $wrap[0] . $newPath . $wrap[1];
537 }
538 }
539 // replace URL paths in content
540 if (!empty($replacements)) {
541 $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
542 }
543 return $contents;
544 }
545
546 /**
547 * Moves @charset, @import and @namespace statements to the top of
548 * the content, because they must occur before all other CSS rules
549 *
550 * @param string $contents Data to process
551 * @return string Processed data
552 */
553 protected function cssFixStatements($contents)
554 {
555 $matches = [];
556 $comment = LF . '/* moved by compressor */' . LF;
557 // nothing to do, so just return contents
558 if (stripos($contents, '@charset') === false && stripos($contents, '@import') === false && stripos($contents, '@namespace') === false) {
559 return $contents;
560 }
561 $regex = '/@(charset|import|namespace)\\s*(url)?\\s*\\(?\\s*["\']?[^"\'\\)]+["\']?\\s*\\)?\\s*;/i';
562 preg_match_all($regex, $contents, $matches);
563 if (!empty($matches[0])) {
564 // Ensure correct order of @charset, @namespace and @import
565 $charset = '';
566 $namespaces = [];
567 $imports = [];
568 foreach ($matches[1] as $index => $keyword) {
569 switch ($keyword) {
570 case 'charset':
571 if (empty($charset)) {
572 $charset = $matches[0][$index];
573 }
574 break;
575 case 'namespace':
576 $namespaces[] = $matches[0][$index];
577 break;
578 case 'import':
579 $imports[] = $matches[0][$index];
580 break;
581 }
582 }
583
584 $namespaces = !empty($namespaces) ? implode('', $namespaces) . $comment : '';
585 $imports = !empty($imports) ? implode('', $imports) . $comment : '';
586 // remove existing statements
587 $contents = str_replace($matches[0], '', $contents);
588 // add statements to the top of contents in the order they occur in original file
589 $contents =
590 $charset
591 . $comment
592 . $namespaces
593 . $imports
594 . trim($contents);
595 }
596 return $contents;
597 }
598
599 /**
600 * Writes $contents into file $filename together with a gzipped version into $filename.gz
601 *
602 * @param string $filename Target filename
603 * @param string $contents File contents
604 */
605 protected function writeFileAndCompressed($filename, $contents)
606 {
607 // write uncompressed file
608 GeneralUtility::writeFile(Environment::getPublicPath() . '/' . $filename, $contents);
609 if ($this->createGzipped) {
610 // create compressed version
611 GeneralUtility::writeFile(Environment::getPublicPath() . '/' . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel));
612 }
613 }
614
615 /**
616 * Decides whether a client can deal with gzipped content or not and returns the according file name,
617 * based on HTTP_ACCEPT_ENCODING
618 *
619 * @param string $filename File name
620 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING
621 */
622 protected function returnFileReference($filename)
623 {
624 // if the client accepts gzip and we can create gzipped files, we give him compressed versions
625 if ($this->createGzipped && strpos(GeneralUtility::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false) {
626 $filename .= '.gzip';
627 }
628 return PathUtility::getRelativePath($this->rootPath, Environment::getPublicPath() . '/') . $filename;
629 }
630
631 /**
632 * Retrieves an external file and stores it locally.
633 *
634 * @param string $url
635 * @return string Temporary local filename for the externally-retrieved file
636 */
637 protected function retrieveExternalFile($url)
638 {
639 $externalContent = GeneralUtility::getUrl($url);
640 $filename = $this->targetDirectory . 'external-' . md5($url);
641 // Write only if file does not exist OR md5 of the content is not the same as fetched one
642 if (!file_exists(Environment::getPublicPath() . '/' . $filename)
643 || (md5($externalContent) !== md5(file_get_contents(Environment::getPublicPath() . '/' . $filename)))
644 ) {
645 GeneralUtility::writeFile(Environment::getPublicPath() . '/' . $filename, $externalContent);
646 }
647 return $filename;
648 }
649
650 /**
651 * Compress a CSS string by removing comments and whitespace characters
652 *
653 * @param string $contents
654 * @return string
655 */
656 protected function compressCssString($contents)
657 {
658 // Perform some safe CSS optimizations.
659 // Regexp to match comment blocks.
660 $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
661 // Regexp to match double quoted strings.
662 $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
663 // Regexp to match single quoted strings.
664 $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
665 // Strip all comment blocks, but keep double/single quoted strings.
666 $contents = preg_replace(
667 "<($double_quot|$single_quot)|$comment>Ss",
668 '$1',
669 $contents
670 );
671 // Remove certain whitespace.
672 // There are different conditions for removing leading and trailing
673 // whitespace.
674 // @see http://php.net/manual/regexp.reference.subpatterns.php
675 $contents = preg_replace(
676 '<
677 # Strip leading and trailing whitespace.
678 \s*([@{};,])\s*
679 # Strip only leading whitespace from:
680 # - Closing parenthesis: Retain "@media (bar) and foo".
681 | \s+([\)])
682 # Strip only trailing whitespace from:
683 # - Opening parenthesis: Retain "@media (bar) and foo".
684 # - Colon: Retain :pseudo-selectors.
685 | ([\(:])\s+
686 >xS',
687 // Only one of the three capturing groups will match, so its reference
688 // will contain the wanted value and the references for the
689 // two non-matching groups will be replaced with empty strings.
690 '$1$2$3',
691 $contents
692 );
693 // End the file with a new line.
694 $contents = trim($contents);
695 // Ensure file ends in newline.
696 $contents .= LF;
697 return $contents;
698 }
699 }