Merged ImageFunctions.php into GlobalFunctions.php
[lhc/web/wiklou.git] / includes / libs / CSSMin.php
index 3b8171c..4f4b28b 100644 (file)
@@ -1,43 +1,45 @@
 <?php
 /**
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
+ * Copyright 2010 Wikimedia Foundation
  *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
  *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
+ *             http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed
+ * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
+ * OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
  */
 
 /**
  * Transforms CSS data
- * 
+ *
  * This class provides minification, URL remapping, URL extracting, and data-URL embedding.
- * 
- * @author Trevor Parscal
+ *
+ * @file
+ * @version 0.1.1 -- 2010-09-11
+ * @author Trevor Parscal <tparscal@wikimedia.org>
+ * @copyright Copyright 2010 Wikimedia Foundation
+ * @license http://www.apache.org/licenses/LICENSE-2.0
  */
 class CSSMin {
-       
+
        /* Constants */
-       
+
        /**
         * Maximum file size to still qualify for in-line embedding as a data-URI
         *
-        * 24,576 is used because Internet Explorer has a 32,768 byte limit for data URIs, which when base64 encoded will
-        * result in a 1/3 increase in size.
+        * 24,576 is used because Internet Explorer has a 32,768 byte limit for data URIs,
+        * which when base64 encoded will result in a 1/3 increase in size.
         */
        const EMBED_SIZE_LIMIT = 24576;
-       const URL_REGEX = 'url\([\'"]?(?<file>[^\?\)\:\'"]*)\??[^\)\'"]*[\'"]?\)';
-       
+       const URL_REGEX = 'url\(\s*[\'"]?(?P<file>[^\?\)\'"]*)(?P<query>\??[^\)\'"]*)[\'"]?\s*\)';
+
        /* Protected Static Members */
-       
+
        /** @var array List of common image files extensions and mime-types */
        protected static $mimeTypes = array(
                'gif' => 'image/gif',
@@ -49,9 +51,9 @@ class CSSMin {
                'tiff' => 'image/tiff',
                'xbm' => 'image/x-xbitmap',
        );
-       
+
        /* Static Methods */
-       
+
        /**
         * Gets a list of local file paths which are referenced in a CSS style sheet
         *
@@ -61,70 +63,141 @@ class CSSMin {
         */
        public static function getLocalFileReferences( $source, $path = null ) {
                $files = array();
-               if ( preg_match_all( '/' . self::URL_REGEX . '/', $source, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ) ) {
+               $rFlags = PREG_OFFSET_CAPTURE | PREG_SET_ORDER;
+               if ( preg_match_all( '/' . self::URL_REGEX . '/', $source, $matches, $rFlags ) ) {
                        foreach ( $matches as $match ) {
-                               $file = ( isset( $path ) ? rtrim( $path, '/' ) . '/' : '' ) . "{$match['file'][0]}";
+                               $file = ( isset( $path )
+                                       ? rtrim( $path, '/' ) . '/'
+                                       : '' ) . "{$match['file'][0]}";
 
                                // Only proceed if we can access the file
-                               if ( file_exists( $file ) ) {
+                               if ( !is_null( $path ) && file_exists( $file ) ) {
                                        $files[] = $file;
                                }
                        }
                }
                return $files;
        }
-       
+
+       /**
+        * @param $file string
+        * @return bool|string
+        */
+       protected static function getMimeType( $file ) {
+               $realpath = realpath( $file );
+               // Try a couple of different ways to get the mime-type of a file, in order of
+               // preference
+               if (
+                       $realpath
+                       && function_exists( 'finfo_file' )
+                       && function_exists( 'finfo_open' )
+                       && defined( 'FILEINFO_MIME_TYPE' )
+               ) {
+                       // As of PHP 5.3, this is how you get the mime-type of a file; it uses the Fileinfo
+                       // PECL extension
+                       return finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $realpath );
+               } elseif ( function_exists( 'mime_content_type' ) ) {
+                       // Before this was deprecated in PHP 5.3, this was how you got the mime-type of a file
+                       return mime_content_type( $file );
+               } else {
+                       // Worst-case scenario has happened, use the file extension to infer the mime-type
+                       $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
+                       if ( isset( self::$mimeTypes[$ext] ) ) {
+                               return self::$mimeTypes[$ext];
+                       }
+               }
+               return false;
+       }
+
        /**
-        * Remaps CSS URL paths and automatically embeds data URIs for URL rules preceded by an /* @embed * / comment
+        * Remaps CSS URL paths and automatically embeds data URIs for URL rules
+        * preceded by an /* @embed * / comment
         *
         * @param $source string CSS data to remap
-        * @param $path string File path where the source was read from
+        * @param $local string File path where the source was read from
+        * @param $remote string URL path to the file
+        * @param $embedData bool If false, never do any data URI embedding, even if / * @embed * / is found
         * @return string Remapped CSS data
         */
-       public static function remap( $source, $path, $embed = true ) {
-               $pattern = '/((?<embed>\s*\/\*\s*\@embed\s*\*\/)(?<pre>[^\;\}]*))?' . self::URL_REGEX . '(?<post>[^;]*)[\;]?/';
+       public static function remap( $source, $local, $remote, $embedData = true ) {
+               $pattern = '/((?P<embed>\s*\/\*\s*\@embed\s*\*\/)(?P<pre>[^\;\}]*))?' .
+                       self::URL_REGEX . '(?P<post>[^;]*)[\;]?/';
                $offset = 0;
                while ( preg_match( $pattern, $source, $match, PREG_OFFSET_CAPTURE, $offset ) ) {
+                       // Skip fully-qualified URLs and data URIs
+                       $urlScheme = parse_url( $match['file'][0], PHP_URL_SCHEME );
+                       if ( $urlScheme ) {
+                               // Move the offset to the end of the match, leaving it alone
+                               $offset = $match[0][1] + strlen( $match[0][0] );
+                               continue;
+                       }
+                       // URLs with absolute paths like /w/index.php need to be expanded
+                       // to absolute URLs but otherwise left alone
+                       if ( $match['file'][0] !== '' && $match['file'][0][0] === '/' ) {
+                               // Replace the file path with an expanded (possibly protocol-relative) URL
+                               // ...but only if wfExpandUrl() is even available.
+                               // This will not be the case if we're running outside of MW
+                               $lengthIncrease = 0;
+                               if ( function_exists( 'wfExpandUrl' ) ) {
+                                       $expanded = wfExpandUrl( $match['file'][0], PROTO_RELATIVE );
+                                       $origLength = strlen( $match['file'][0] );
+                                       $lengthIncrease = strlen( $expanded ) - $origLength;
+                                       $source = substr_replace( $source, $expanded,
+                                               $match['file'][1], $origLength
+                                       );
+                               }
+                               // Move the offset to the end of the match, leaving it alone
+                               $offset = $match[0][1] + strlen( $match[0][0] ) + $lengthIncrease;
+                               continue;
+                       }
                        // Shortcuts
                        $embed = $match['embed'][0];
                        $pre = $match['pre'][0];
                        $post = $match['post'][0];
-                       $file = "{$path}/{$match['file'][0]}";
-                       // Only proceed if we can access the file
-                       if ( file_exists( $file ) ) {
-                               // Add version parameter as a time-stamp in ISO 8601 format, using Z for the timezone, meaning GMT
-                               $url = "{$file}?" . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $file ), -2 ) );
-                               // If we the mime-type can't be determined, no embedding will take place
-                               $type = false;
-                               // Try a couple of different ways to get the mime-type of a file, in order of preference
-                               if ( function_exists( 'finfo_file' ) && function_exists( 'finfo_open' ) ) {
-                                       // As of PHP 5.3, this is how you get the mime-type of a file; it uses the Fileinfo PECL extension
-                                       $type = finfo_file( finfo_open( FILEINFO_MIME_TYPE ), $file );
-                               } else if ( function_exists( 'mime_content_type' ) ) {
-                                       // Before this was deprecated in PHP 5.3, this used to be how you get the mime-type of a file
-                                       $type = mime_content_type( $file );
-                               } else {
-                                       // Worst-case scenario has happend, use the file extension to infer the mime-type
-                                       $ext = strtolower( pathinfo( $file, PATHINFO_EXTENSION ) );
-                                       if ( isset( self::$mimeTypes[$ext] ) ) {
-                                               $type = self::$mimeTypes[$ext];
+                       $query = $match['query'][0];
+                       $url = "{$remote}/{$match['file'][0]}";
+                       $file = "{$local}/{$match['file'][0]}";
+                       // bug 27052 - Guard against double slashes, because foo//../bar
+                       // apparently resolves to foo/bar on (some?) clients
+                       $url = preg_replace( '#([^:])//+#', '\1/', $url );
+                       $replacement = false;
+                       if ( $local !== false && file_exists( $file ) ) {
+                               // Add version parameter as a time-stamp in ISO 8601 format,
+                               // using Z for the timezone, meaning GMT
+                               $url .= '?' . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $file ), -2 ) );
+                               // Embedding requires a bit of extra processing, so let's skip that if we can
+                               if ( $embedData && $embed ) {
+                                       $type = self::getMimeType( $file );
+                                       // Detect when URLs were preceeded with embed tags, and also verify file size is
+                                       // below the limit
+                                       if (
+                                               $type
+                                               && $match['embed'][1] > 0
+                                               && filesize( $file ) < self::EMBED_SIZE_LIMIT
+                                       ) {
+                                               // Strip off any trailing = symbols (makes browsers freak out)
+                                               $data = base64_encode( file_get_contents( $file ) );
+                                               // Build 2 CSS properties; one which uses a base64 encoded data URI in place
+                                               // of the @embed comment to try and retain line-number integrity, and the
+                                               // other with a remapped an versioned URL and an Internet Explorer hack
+                                               // making it ignored in all browsers that support data URIs
+                                               $replacement = "{$pre}url(data:{$type};base64,{$data}){$post};";
+                                               $replacement .= "{$pre}url({$url}){$post}!ie;";
                                        }
                                }
-                               // Detect when URLs were preceeded with embed tags, and also verify file size is below the limit
-                               if ( $embed && $type && $match['embed'][1] > 0 && filesize( $file ) < self::EMBED_SIZE_LIMIT ) {
-                                       // Strip off any trailing = symbols (makes browsers freak out)
-                                       $data = base64_encode( file_get_contents( $file ) );
-                                       // Build 2 CSS properties; one which uses a base64 encoded data URI in place of the @embed
-                                       // comment to try and retain line-number integrity , and the other with a remapped an versioned
-                                       // URL and an Internet Explorer hack making it ignored in all browsers that support data URIs
-                                       $replacement = "{$pre}url(data:{$type};base64,{$data}){$post};{$pre}url({$url}){$post}!ie;";
-                               } else {
-                                       // Build a CSS property with a remapped and versioned URL, preserving comment for debug mode
+                               if ( $replacement === false ) {
+                                       // Assume that all paths are relative to $remote, and make them absolute
                                        $replacement = "{$embed}{$pre}url({$url}){$post};";
                                }
-
+                       } elseif ( $local === false ) {
+                               // Assume that all paths are relative to $remote, and make them absolute
+                               $replacement = "{$embed}{$pre}url({$url}{$query}){$post};";
+                       }
+                       if ( $replacement !== false ) {
                                // Perform replacement on the source
-                               $source = substr_replace( $source, $replacement, $match[0][1], strlen( $match[0][0] ) );
+                               $source = substr_replace(
+                                       $source, $replacement, $match[0][1], strlen( $match[0][0] )
+                               );
                                // Move the offset to the end of the replacement in the source
                                $offset = $match[0][1] + strlen( $replacement );
                                continue;
@@ -134,11 +207,11 @@ class CSSMin {
                }
                return $source;
        }
-       
+
        /**
         * Removes whitespace from CSS data
         *
-        * @param $source string CSS data to minify
+        * @param $css string CSS data to minify
         * @return string Minified CSS data
         */
        public static function minify( $css ) {