MediaWiki  REL1_23
CSSMin.php
Go to the documentation of this file.
00001 <?php
00030 class CSSMin {
00031 
00032     /* Constants */
00033 
00040     const EMBED_SIZE_LIMIT = 24576;
00041     const URL_REGEX = 'url\(\s*[\'"]?(?P<file>[^\?\)\'"]*?)(?P<query>\?[^\)\'"]*?|)[\'"]?\s*\)';
00042     const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/';
00043 
00044     /* Protected Static Members */
00045 
00047     protected static $mimeTypes = array(
00048         'gif' => 'image/gif',
00049         'jpe' => 'image/jpeg',
00050         'jpeg' => 'image/jpeg',
00051         'jpg' => 'image/jpeg',
00052         'png' => 'image/png',
00053         'tif' => 'image/tiff',
00054         'tiff' => 'image/tiff',
00055         'xbm' => 'image/x-xbitmap',
00056         'svg' => 'image/svg+xml',
00057     );
00058 
00059     /* Static Methods */
00060 
00071     public static function getLocalFileReferences( $source, $path = null ) {
00072         if ( $path === null ) {
00073             return array();
00074         }
00075 
00076         $path = rtrim( $path, '/' ) . '/';
00077         $files = array();
00078 
00079         $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
00080         if ( preg_match_all( '/' . self::URL_REGEX . '/', $source, $matches, $rFlags ) ) {
00081             foreach ( $matches as $match ) {
00082                 $url = $match['file'][0];
00083 
00084                 // Skip fully-qualified and protocol-relative URLs and data URIs
00085                 if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) {
00086                     break;
00087                 }
00088 
00089                 $file = $path . $url;
00090                 // Skip non-existent files
00091                 if ( file_exists( $file ) ) {
00092                     break;
00093                 }
00094 
00095                 $files[] = $file;
00096             }
00097         }
00098         return $files;
00099     }
00100 
00115     public static function encodeImageAsDataURI( $file, $type = null, $sizeLimit = self::EMBED_SIZE_LIMIT ) {
00116         if ( $sizeLimit !== false && filesize( $file ) >= $sizeLimit ) {
00117             return false;
00118         }
00119         if ( $type === null ) {
00120             $type = self::getMimeType( $file );
00121         }
00122         if ( !$type ) {
00123             return false;
00124         }
00125         $data = base64_encode( file_get_contents( $file ) );
00126         return 'data:' . $type . ';base64,' . $data;
00127     }
00128 
00133     public static function getMimeType( $file ) {
00134         $realpath = realpath( $file );
00135         // Try a couple of different ways to get the mime-type of a file, in order of
00136         // preference
00137         if (
00138             $realpath
00139             && function_exists( 'finfo_file' )
00140             && function_exists( 'finfo_open' )
00141             && defined( 'FILEINFO_MIME_TYPE' )
00142         ) {
00143             // As of PHP 5.3, this is how you get the mime-type of a file; it uses the Fileinfo
00144             // PECL extension
00145             return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath );
00146         } elseif ( function_exists( 'mime_content_type' ) ) {
00147             // Before this was deprecated in PHP 5.3, this was how you got the mime-type of a file
00148             return mime_content_type( $file );
00149         } else {
00150             // Worst-case scenario has happened, use the file extension to infer the mime-type
00151             $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
00152             if ( isset( self::$mimeTypes[$ext] ) ) {
00153                 return self::$mimeTypes[$ext];
00154             }
00155         }
00156         return false;
00157     }
00158 
00166     public static function buildUrlValue( $url ) {
00167         // The list below has been crafted to match URLs such as:
00168         //   scheme://user@domain:port/~user/fi%20le.png?query=yes&really=y+s
00169         //   data:image/png;base64,R0lGODlh/+==
00170         if ( preg_match( '!^[\w\d:@/~.%+;,?&=-]+$!', $url ) ) {
00171             return "url($url)";
00172         } else {
00173             return 'url("' . strtr( $url, array( '\\' => '\\\\', '"' => '\\"' ) ) . '")';
00174         }
00175     }
00176 
00187     public static function remap( $source, $local, $remote, $embedData = true ) {
00188         // High-level overview:
00189         // * For each CSS rule in $source that includes at least one url() value:
00190         //   * Check for an @embed comment at the start indicating that all URIs should be embedded
00191         //   * For each url() value:
00192         //     * Check for an @embed comment directly preceding the value
00193         //     * If either @embed comment exists:
00194         //       * Embedding the URL as data: URI, if it's possible / allowed
00195         //       * Otherwise remap the URL to work in generated stylesheets
00196 
00197         // Guard against trailing slashes, because "some/remote/../foo.png"
00198         // resolves to "some/remote/foo.png" on (some?) clients (bug 27052).
00199         if ( substr( $remote, -1 ) == '/' ) {
00200             $remote = substr( $remote, 0, -1 );
00201         }
00202 
00203         // Note: This will not correctly handle cases where ';', '{' or '}' appears in the rule itself,
00204         // e.g. in a quoted string. You are advised not to use such characters in file names.
00205         // We also match start/end of the string to be consistent in edge-cases ('@import url(…)').
00206         $pattern = '/(?:^|[;{])\K[^;{}]*' . CSSMin::URL_REGEX . '[^;}]*(?=[;}]|$)/';
00207         return preg_replace_callback( $pattern, function ( $matchOuter ) use ( $local, $remote, $embedData ) {
00208             $rule = $matchOuter[0];
00209 
00210             // Check for global @embed comment and remove it
00211             $embedAll = false;
00212             $rule = preg_replace( '/^(\s*)' . CSSMin::EMBED_REGEX . '\s*/', '$1', $rule, 1, $embedAll );
00213 
00214             // Build two versions of current rule: with remapped URLs and with embedded data: URIs (where possible)
00215             $pattern = '/(?P<embed>' . CSSMin::EMBED_REGEX . '\s*|)' . CSSMin::URL_REGEX . '/';
00216 
00217             $ruleWithRemapped = preg_replace_callback( $pattern, function ( $match ) use ( $local, $remote ) {
00218                 $remapped = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, false );
00219                 return CSSMin::buildUrlValue( $remapped );
00220             }, $rule );
00221 
00222             if ( $embedData ) {
00223                 $ruleWithEmbedded = preg_replace_callback( $pattern, function ( $match ) use ( $embedAll, $local, $remote ) {
00224                     $embed = $embedAll || $match['embed'];
00225                     $embedded = CSSMin::remapOne( $match['file'], $match['query'], $local, $remote, $embed );
00226                     return CSSMin::buildUrlValue( $embedded );
00227                 }, $rule );
00228             }
00229 
00230             if ( $embedData && $ruleWithEmbedded !== $ruleWithRemapped ) {
00231                 // Build 2 CSS properties; one which uses a base64 encoded data URI in place
00232                 // of the @embed comment to try and retain line-number integrity, and the
00233                 // other with a remapped an versioned URL and an Internet Explorer hack
00234                 // making it ignored in all browsers that support data URIs
00235                 return "$ruleWithEmbedded;$ruleWithRemapped!ie";
00236             } else {
00237                 // No reason to repeat twice
00238                 return $ruleWithRemapped;
00239             }
00240         }, $source );
00241     }
00242 
00253     public static function remapOne( $file, $query, $local, $remote, $embed ) {
00254         // The full URL possibly with query, as passed to the 'url()' value in CSS
00255         $url = $file . $query;
00256 
00257         // Skip fully-qualified and protocol-relative URLs and data URIs
00258         if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) {
00259             return $url;
00260         }
00261 
00262         // URLs with absolute paths like /w/index.php need to be expanded
00263         // to absolute URLs but otherwise left alone
00264         if ( $url !== '' && $url[0] === '/' ) {
00265             // Replace the file path with an expanded (possibly protocol-relative) URL
00266             // ...but only if wfExpandUrl() is even available.
00267             // This will not be the case if we're running outside of MW
00268             if ( function_exists( 'wfExpandUrl' ) ) {
00269                 return wfExpandUrl( $url, PROTO_RELATIVE );
00270             } else {
00271                 return $url;
00272             }
00273         }
00274 
00275         if ( $local === false ) {
00276             // Assume that all paths are relative to $remote, and make them absolute
00277             return $remote . '/' . $url;
00278         } else {
00279             // We drop the query part here and instead make the path relative to $remote
00280             $url = "{$remote}/{$file}";
00281             // Path to the actual file on the filesystem
00282             $localFile = "{$local}/{$file}";
00283             if ( file_exists( $localFile ) ) {
00284                 // Add version parameter as a time-stamp in ISO 8601 format,
00285                 // using Z for the timezone, meaning GMT
00286                 $url .= '?' . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $localFile ), -2 ) );
00287                 if ( $embed ) {
00288                     $data = self::encodeImageAsDataURI( $localFile );
00289                     if ( $data !== false ) {
00290                         return $data;
00291                     }
00292                 }
00293             }
00294             // If any of these conditions failed (file missing, we don't want to embed it
00295             // or it's not embeddable), return the URL (possibly with ?timestamp part)
00296             return $url;
00297         }
00298     }
00299 
00306     public static function minify( $css ) {
00307         return trim(
00308             str_replace(
00309                 array( '; ', ': ', ' {', '{ ', ', ', '} ', ';}' ),
00310                 array( ';', ':', '{', '{', ',', '}', '}' ),
00311                 preg_replace( array( '/\s+/', '/\/\*.*?\*\//s' ), array( ' ', '' ), $css )
00312             )
00313         );
00314     }
00315 }