|
TYPO3 API
SVNRelease
|
00001 <?php 00002 /*************************************************************** 00003 * Copyright notice 00004 * 00005 * (c) 2010-2011 Steffen Gebert (steffen@steffen-gebert.de) 00006 * All rights reserved 00007 * 00008 * This script is part of the TYPO3 project. The TYPO3 project is 00009 * free software; you can redistribute it and/or modify 00010 * it under the terms of the GNU General Public License as published by 00011 * the Free Software Foundation; either version 2 of the License, or 00012 * (at your option) any later version. 00013 * 00014 * The GNU General Public License can be found at 00015 * http://www.gnu.org/copyleft/gpl.html. 00016 * A copy is found in the textfile GPL.txt and important notices to the license 00017 * from the author is found in LICENSE.txt distributed with these scripts. 00018 * 00019 * 00020 * This script is distributed in the hope that it will be useful, 00021 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00023 * GNU General Public License for more details. 00024 * 00025 * This copyright notice MUST APPEAR in all copies of the script! 00026 ***************************************************************/ 00027 00028 /** 00029 * Compressor 00030 * This merges and compresses CSS and JavaScript files of the TYPO3 Backend. 00031 * 00032 * @author Steffen Gebert <steffen@steffen-gebert.de> 00033 * @package TYPO3 00034 * @subpackage t3lib 00035 * $Id$ 00036 */ 00037 class t3lib_Compressor { 00038 00039 protected $targetDirectory = 'typo3temp/compressor/'; 00040 00041 // gzipped versions are only created if $TYPO3_CONF_VARS[TYPO3_MODE]['compressionLevel'] is set 00042 protected $createGzipped = FALSE; 00043 // default compression level is -1 00044 protected $gzipCompressionLevel = -1; 00045 00046 protected $htaccessTemplate = '<FilesMatch "\.(js|css)(\.gzip)?$"> 00047 <IfModule mod_expires.c> 00048 ExpiresActive on 00049 ExpiresDefault "access plus 7 days" 00050 </IfModule> 00051 FileETag MTime Size 00052 </FilesMatch>'; 00053 00054 /** 00055 * Constructor 00056 */ 00057 public function __construct() { 00058 00059 // we check for existance of our targetDirectory 00060 if (!is_dir(PATH_site . $this->targetDirectory)) { 00061 t3lib_div::mkdir(PATH_site . $this->targetDirectory); 00062 } 00063 00064 // if enabled, we check whether we should auto-create the .htaccess file 00065 if ($GLOBALS['TYPO3_CONF_VARS']['SYS']['generateApacheHtaccess']) { 00066 // check whether .htaccess exists 00067 $htaccessPath = PATH_site . $this->targetDirectory . '.htaccess'; 00068 if (!file_exists($htaccessPath)) { 00069 t3lib_div::writeFile($htaccessPath, $this->htaccessTemplate); 00070 } 00071 } 00072 00073 // decide whether we should create gzipped versions or not 00074 $compressionLevel = $GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['compressionLevel']; 00075 // we need zlib for gzencode() 00076 if (extension_loaded('zlib') && $compressionLevel) { 00077 $this->createGzipped = TRUE; 00078 // $compressionLevel can also be TRUE 00079 if (t3lib_div::testInt($compressionLevel)) { 00080 $this->gzipCompressionLevel = intval($compressionLevel); 00081 } 00082 } 00083 } 00084 00085 /** 00086 * Concatenates the cssFiles 00087 * 00088 * Options: 00089 * baseDirectories If set, only include files below one of the base directories 00090 * 00091 * @param array $cssFiles CSS files to process 00092 * @param array $options Additional options 00093 * @return array CSS files 00094 */ 00095 public function concatenateCssFiles(array $cssFiles, $options = array()) { 00096 00097 $filesToInclude = array(); 00098 foreach ($cssFiles as $filename => $fileOptions) { 00099 // we remove BACK_PATH from $filename, so make it relative to TYPO3_mainDir 00100 $filenameFromMainDir = $this->getFilenameFromMainDir($filename); 00101 // if $options['baseDirectories'] set, we only include files below these directories 00102 if ((!isset($options['baseDirectories']) 00103 || $this->checkBaseDirectory($filenameFromMainDir, array_merge($options['baseDirectories'], array($this->targetDirectory)))) 00104 && ($fileOptions['media'] === 'all') 00105 ) { 00106 00107 $filesToInclude[] = $filenameFromMainDir; 00108 // remove the file from the incoming file array 00109 unset($cssFiles[$filename]); 00110 } 00111 } 00112 00113 if (count($filesToInclude)) { 00114 $targetFile = $this->createMergedCssFile($filesToInclude); 00115 $concatenatedOptions = array( 00116 'rel' => 'stylesheet', 00117 'media' => 'all', 00118 'compress' => TRUE, 00119 ); 00120 $targetFileRelative = $GLOBALS['BACK_PATH'] . '../' . $targetFile; 00121 // place the merged stylesheet on top of the stylesheets 00122 $cssFiles = array_merge(array($targetFileRelative => $concatenatedOptions), $cssFiles); 00123 } 00124 return $cssFiles; 00125 } 00126 00127 /** 00128 * Finds the relative path to a file, relative to the TYPO3_mainDir. 00129 * 00130 * @param string $filename the name of the file 00131 * @return string the path to the file relative to the TYPO3_mainDir 00132 */ 00133 private function getFilenameFromMainDir($filename) { 00134 // if the file exists in the typo3/ folder or the BACK_PATH is empty, just return the $filename 00135 if (substr($filename, 0, strlen($GLOBALS['BACK_PATH'])) === $GLOBALS['BACK_PATH']) { 00136 $file = str_replace($GLOBALS['BACK_PATH'], '', $filename); 00137 if (is_file(PATH_typo3 . $file) || empty($GLOBALS['BACK_PATH'])) { 00138 return $file; 00139 } 00140 } 00141 00142 // build the file path relatively to the PATH_site 00143 $backPath = str_replace(TYPO3_mainDir, '', $GLOBALS['BACK_PATH']); 00144 $file = str_replace($backPath, '', $filename); 00145 if (substr($file, 0, 3) === '../') { 00146 $file = t3lib_div::resolveBackPath(PATH_typo3 . $file); 00147 } else { 00148 $file = PATH_site . $file; 00149 } 00150 00151 // check if the file exists, and if so, return the path relative to TYPO3_mainDir 00152 if (is_file($file)) { 00153 $mainDirDepth = substr_count(TYPO3_mainDir, '/'); 00154 return str_repeat('../', $mainDirDepth) . str_replace(PATH_site, '', $file); 00155 } 00156 00157 // none of above conditions were met, fallback to default behaviour 00158 return substr($filename, strlen($GLOBALS['BACK_PATH'])); 00159 } 00160 00161 /** 00162 * Creates a merged CSS file 00163 * 00164 * @param array $filesToInclude Files which should be merged, paths relative to TYPO3_mainDir 00165 * @return mixed Filename of the merged file 00166 */ 00167 protected function createMergedCssFile(array $filesToInclude) { 00168 // we add up the filenames, filemtimes and filsizes to later build a checksum over 00169 // it and include it in the temporary file name 00170 $unique = ''; 00171 00172 foreach ($filesToInclude as $filename) { 00173 $filepath = t3lib_div::resolveBackPath(PATH_typo3 . $filename); 00174 $unique .= $filename . filemtime($filepath) . filesize($filepath); 00175 } 00176 $targetFile = $this->targetDirectory . 'merged-' . md5($unique) . '.css'; 00177 00178 // if the file doesn't already exist, we create it 00179 if (!file_exists(PATH_site . $targetFile)) { 00180 $concatenated = ''; 00181 // concatenate all the files together 00182 foreach ($filesToInclude as $filename) { 00183 $contents = t3lib_div::getUrl(t3lib_div::resolveBackPath(PATH_typo3 . $filename)); 00184 // only fix paths if files aren't already in typo3temp (already processed) 00185 if (!t3lib_div::isFirstPartOfStr($filename, $this->targetDirectory)) { 00186 $concatenated .= $this->cssFixRelativeUrlPaths($contents, dirname($filename) . '/'); 00187 } else { 00188 $concatenated .= $contents; 00189 } 00190 } 00191 t3lib_div::writeFile(PATH_site . $targetFile, $concatenated); 00192 } 00193 return $targetFile; 00194 } 00195 00196 /** 00197 * Compress multiple css files 00198 * 00199 * @param array $cssFiles The files to compress (array key = filename), relative to requested page 00200 * @return array The CSS files after compression (array key = new filename), relative to requested page 00201 */ 00202 public function compressCssFiles(array $cssFiles) { 00203 $filesAfterCompression = array(); 00204 foreach ($cssFiles as $filename => $fileOptions) { 00205 // if compression is enabled 00206 if ($fileOptions['compress']) { 00207 $filesAfterCompression[$this->compressCssFile($filename)] = $fileOptions; 00208 } else { 00209 $filesAfterCompression[$filename] = $fileOptions; 00210 } 00211 } 00212 return $filesAfterCompression; 00213 } 00214 00215 /** 00216 * Compresses a CSS file 00217 * 00218 * Options: 00219 * baseDirectories If set, only include files below one of the base directories 00220 * 00221 * removes comments and whitespaces 00222 * Adopted from http://drupal.org/files/issues/minify_css.php__1.txt 00223 * 00224 * @param string $filename Source filename, relative to requested page 00225 * @return string Compressed filename, relative to requested page 00226 */ 00227 public function compressCssFile($filename) { 00228 // generate the unique name of the file 00229 $filenameAbsolute = t3lib_div::resolveBackPath(PATH_typo3 . substr($filename, strlen($GLOBALS['BACK_PATH']))); 00230 $unique = $filenameAbsolute . filemtime($filenameAbsolute) . filesize($filenameAbsolute); 00231 00232 $pathinfo = pathinfo($filename); 00233 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.css'; 00234 // only create it, if it doesn't exist, yet 00235 if (!file_exists(PATH_site . $targetFile) || ($this->createGzipped && !file_exists(PATH_site . $targetFile . '.gzip'))) { 00236 $contents = t3lib_div::getUrl($filenameAbsolute); 00237 // Perform some safe CSS optimizations. 00238 $contents = str_replace("\r", '', $contents); // Strip any and all carriage returns. 00239 // Match and process strings, comments and everything else, one chunk at a time. 00240 // To understand this regex, read: "Mastering Regular Expressions 3rd Edition" chapter 6. 00241 $contents = preg_replace_callback('% 00242 # One-regex-to-rule-them-all! - version: 20100220_0100 00243 # Group 1: Match a double quoted string. 00244 ("[^"\\\\]*+(?:\\\\.[^"\\\\]*+)*+") | # or... 00245 # Group 2: Match a single quoted string. 00246 (\'[^\'\\\\]*+(?:\\\\.[^\'\\\\]*+)*+\') | # or... 00247 # Group 3: Match a regular non-MacIE5-hack comment. 00248 (/\*[^\\\\*]*+\*++(?:[^\\\\*/][^\\\\*]*+\*++)*+/) | # or... 00249 # Group 4: Match a MacIE5-type1 comment. 00250 (/\*(?:[^*\\\\]*+\**+(?!/))*+\\\\[^*]*+\*++(?:[^*/][^*]*+\*++)*+/(?<!\\\\\*/)) | # or... 00251 # Group 5: Match a MacIE5-type2 comment. 00252 (/\*[^*]*\*+(?:[^/*][^*]*\*+)*/(?<=\\\\\*/)) # folllowed by... 00253 # Group 6: Match everything up to final closing regular comment 00254 ([^/]*+(?:(?!\*)/[^/]*+)*?) 00255 # Group 7: Match final closing regular comment 00256 (/\*[^/]++(?:(?<!\*)/(?!\*)[^/]*+)*+/(?<=(?<!\\\\)\*/)) | # or... 00257 # Group 8: Match regular non-string, non-comment text. 00258 ([^"\'/]*+(?:(?!/\*)/[^"\'/]*+)*+) 00259 %Ssx', array('self', 'compressCssPregCallback'), $contents); // Do it! 00260 $contents = preg_replace('/^\s++/', '', $contents); // Strip leading whitespace. 00261 $contents = preg_replace('/[ \t]*+\n\s*+/S', "\n", $contents); // Consolidate multi-lines space. 00262 $contents = preg_replace('/(?<!\s)\s*+$/S', "\n", $contents); // Ensure file ends in newline. 00263 // we have to fix relative paths, if we aren't working on a file in our target directory 00264 if (!is_int(strpos($filename, $this->targetDirectory))) { 00265 $filenameRelativeToMainDir = substr($filename, strlen($GLOBALS['BACK_PATH'])); 00266 $contents = $this->cssFixRelativeUrlPaths($contents, dirname($filenameRelativeToMainDir) . '/'); 00267 } 00268 $this->writeFileAndCompressed($targetFile, $contents); 00269 } 00270 00271 return $GLOBALS['BACK_PATH'] . '../' . $this->returnFileReference($targetFile); 00272 } 00273 00274 /** 00275 * Callback function for preg_replace 00276 * 00277 * @see compressCssFile 00278 * @param array $matches 00279 * @return string the compressed string 00280 */ 00281 public static function compressCssPregCallback($matches) { 00282 if ($matches[1]) { // Group 1: Double quoted string. 00283 return $matches[1]; // Return the string unmodified. 00284 } elseif ($matches[2]) { // Group 2: Single quoted string. 00285 return $matches[2]; // Return the string unmodified. 00286 } elseif ($matches[3]) { // Group 3: Regular non-MacIE5-hack comment. 00287 return "\n"; // Return single space. 00288 } elseif ($matches[4]) { // Group 4: MacIE5-hack-type-1 comment. 00289 return "\n/*\\T1*/\n"; // Return minimal MacIE5-hack-type-1 comment. 00290 } 00291 elseif ($matches[5]) { // Group 5,6,7: MacIE5-hack-type-2 comment 00292 $matches[6] = preg_replace('/\s++([+>{};,)])/S', '$1', $matches[6]); // Clean pre-punctuation. 00293 $matches[6] = preg_replace('/([+>{}:;,(])\s++/S', '$1', $matches[6]); // Clean post-punctuation. 00294 $matches[6] = preg_replace('/;?\}/S', "}\n", $matches[6]); // Add a touch of formatting. 00295 return "\n/*T2\\*/" . $matches[6] . "\n/*T2E*/\n"; // Minify and reassemble composite type2 comment. 00296 } elseif (isset($matches[8])) { // Group 8: Non-string, non-comment. Safe to clean whitespace here. 00297 $matches[8] = preg_replace('/^\s++/', '', $matches[8]); // Strip all leading whitespace. 00298 $matches[8] = preg_replace('/\s++$/', '', $matches[8]); // Strip all trailing whitespace. 00299 $matches[8] = preg_replace('/\s{2,}+/', ' ', $matches[8]); // Consolidate multiple whitespace. 00300 $matches[8] = preg_replace('/\s++([+>{};,)])/S', '$1', $matches[8]); // Clean pre-punctuation. 00301 $matches[8] = preg_replace('/([+>{}:;,(])\s++/S', '$1', $matches[8]); // Clean post-punctuation. 00302 $matches[8] = preg_replace('/;?\}/S', "}\n", $matches[8]); // Add a touch of formatting. 00303 return $matches[8]; 00304 } 00305 return $matches[0] . "\n/* ERROR! Unexpected _proccess_css_minify() parameter */\n"; // never get here 00306 } 00307 00308 /** 00309 * Compress multiple javascript files 00310 * 00311 * @param array $jsFiles The files to compress (array key = filename), relative to requested page 00312 * @return array The js files after compression (array key = new filename), relative to requested page 00313 */ 00314 public function compressJsFiles(array $jsFiles) { 00315 $filesAfterCompression = array(); 00316 foreach ($jsFiles as $filename => $fileOptions) { 00317 // we remove BACK_PATH from $filename, so make it relative to TYPO3_mainDir 00318 $filenameFromMainDir = $this->getFilenameFromMainDir($filename); 00319 // if compression is enabled 00320 if ($fileOptions['compress']) { 00321 $filesAfterCompression[$this->compressJsFile($filename)] = $fileOptions; 00322 } else { 00323 $filesAfterCompression[$filename] = $fileOptions; 00324 } 00325 } 00326 return $filesAfterCompression; 00327 } 00328 00329 /** 00330 * Compresses a javascript file 00331 * 00332 * Options: 00333 * baseDirectories If set, only include files below one of the base directories 00334 * 00335 * @param string $filename Source filename, relative to requested page 00336 * @return string Filename of the compressed file, relative to requested page 00337 */ 00338 public function compressJsFile($filename) { 00339 // generate the unique name of the file 00340 $filenameAbsolute = t3lib_div::resolveBackPath(PATH_typo3 . $this->getFilenameFromMainDir($filename)); 00341 $unique = $filenameAbsolute . filemtime($filenameAbsolute) . filesize($filenameAbsolute); 00342 00343 $pathinfo = pathinfo($filename); 00344 $targetFile = $this->targetDirectory . $pathinfo['filename'] . '-' . md5($unique) . '.js'; 00345 // only create it, if it doesn't exist, yet 00346 if (!file_exists(PATH_site . $targetFile) || ($this->createGzipped && !file_exists(PATH_site . $targetFile . '.gzip'))) { 00347 $contents = t3lib_div::getUrl($filenameAbsolute); 00348 $this->writeFileAndCompressed($targetFile, $contents); 00349 } 00350 return $GLOBALS['BACK_PATH'] . '../' . $this->returnFileReference($targetFile); 00351 } 00352 00353 /** 00354 * Decides whether a CSS file comes from one of the baseDirectories 00355 * 00356 * @param string $filename Filename 00357 * @return boolean File belongs to a skin or not 00358 */ 00359 protected function checkBaseDirectory($filename, array $baseDirectories) { 00360 foreach ($baseDirectories as $baseDirectory) { 00361 // check, if $filename starts with $skinStylesheetDirectory 00362 if (t3lib_div::isFirstPartOfStr($filename, $baseDirectory)) { 00363 return TRUE; 00364 } 00365 } 00366 return FALSE; 00367 } 00368 00369 /** 00370 * Fixes the relative paths inside of url() references in CSS files 00371 * 00372 * @param string $contents Data to process 00373 * @param string $oldDir Directory of the originial file, relative to TYPO3_mainDir 00374 * @return string Processed data 00375 */ 00376 protected function cssFixRelativeUrlPaths($contents, $oldDir) { 00377 $matches = array(); 00378 00379 preg_match_all('/url(\(\s*["\']?([^"\']+)["\']?\s*\))/iU', $contents, $matches); 00380 foreach ($matches[2] as $matchCount => $match) { 00381 // remove '," or white-spaces around 00382 $match = preg_replace('/[\"\'\s]/', '', $match); 00383 00384 // we must not rewrite paths containing ":", e.g. data URIs (see RFC 2397) 00385 if (strpos($match, ':') === FALSE) { 00386 $newPath = t3lib_div::resolveBackPath('../../' . TYPO3_mainDir . $oldDir . $match); 00387 $contents = str_replace($matches[1][$matchCount], '(\'' . $newPath . '\')', $contents); 00388 } 00389 } 00390 return $contents; 00391 } 00392 00393 /** 00394 * Writes $contents into file $filename together with a gzipped version into $filename.gz 00395 * 00396 * @param string $filename Target filename 00397 * @param strings $contents File contents 00398 * @return void 00399 */ 00400 protected function writeFileAndCompressed($filename, $contents) { 00401 // write uncompressed file 00402 t3lib_div::writeFile(PATH_site . $filename, $contents); 00403 00404 if ($this->createGzipped) { 00405 // create compressed version 00406 t3lib_div::writeFile(PATH_site . $filename . '.gzip', gzencode($contents, $this->gzipCompressionLevel)); 00407 } 00408 } 00409 00410 /** 00411 * Decides whether a client can deal with gzipped content or not and returns the according file name, 00412 * based on HTTP_ACCEPT_ENCODING 00413 * 00414 * @param string $filename File name 00415 * @return string $filename suffixed with '.gzip' or not - dependent on HTTP_ACCEPT_ENCODING 00416 */ 00417 protected function returnFileReference($filename) { 00418 // if the client accepts gzip and we can create gzipped files, we give him compressed versions 00419 if ($this->createGzipped && strpos(t3lib_div::getIndpEnv('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE) { 00420 return $filename . '.gzip'; 00421 } else { 00422 return $filename; 00423 } 00424 } 00425 } 00426 00427 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_compressor.php'])) { 00428 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/class.t3lib_compressor.php']); 00429 } 00430 00431 ?>
1.8.0