TYPO3 API  SVNRelease
class.t3lib_compressor.php
Go to the documentation of this file.
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 ?>