Merge "Changed some DatabaseBase type hints to IDatabase"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 9 Dec 2014 19:42:08 +0000 (19:42 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 9 Dec 2014 19:42:08 +0000 (19:42 +0000)
13 files changed:
autoload.php
docs/hooks.txt
includes/api/ApiPageSet.php
includes/cache/ResourceFileCache.php
includes/filerepo/file/File.php
includes/profiler/ProfilerXhprof.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderContext.php
includes/resourceloader/ResourceLoaderImage.php [new file with mode: 0644]
includes/resourceloader/ResourceLoaderImageModule.php [new file with mode: 0644]
maintenance/findHooks.php
maintenance/updateArticleCount.php
tests/parser/parserTests.txt

index 8003284..c276b3b 100644 (file)
@@ -951,6 +951,8 @@ $wgAutoloadLocalClasses = array(
        'ResourceLoaderFileModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFileModule.php',
        'ResourceLoaderFilePageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePageModule.php',
        'ResourceLoaderFilePath' => __DIR__ . '/includes/resourceloader/ResourceLoaderFilePath.php',
+       'ResourceLoaderImage' => __DIR__ . '/includes/resourceloader/ResourceLoaderImage.php',
+       'ResourceLoaderImageModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderImageModule.php',
        'ResourceLoaderLanguageDataModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageDataModule.php',
        'ResourceLoaderLanguageNamesModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderLanguageNamesModule.php',
        'ResourceLoaderModule' => __DIR__ . '/includes/resourceloader/ResourceLoaderModule.php',
index 062a0c8..f8b077e 100644 (file)
@@ -2424,7 +2424,7 @@ after variants have been added.
 'SkinTemplateOutputPageBeforeExec': Before SkinTemplate::outputPage() starts
 page output.
 &$sktemplate: SkinTemplate object
-&$tpl: Template engine object
+&$tpl: QuickTemplate engine object
 
 'SkinTemplatePreventOtherActiveTabs': Use this to prevent showing active tabs.
 $sktemplate: SkinTemplate object
index 78c33ed..39cd6d9 100644 (file)
@@ -1334,8 +1334,7 @@ class ApiPageSet extends ApiBase {
                if ( !$this->mAllowGenerator ) {
                        unset( $result['generator'] );
                } elseif ( $flags & ApiBase::GET_VALUES_FOR_HELP ) {
-                       $result['generator'][ApiBase::PARAM_TYPE] = $this->getGenerators();
-                       foreach ( $result['generator'][ApiBase::PARAM_TYPE] as $g ) {
+                       foreach ( $this->getGenerators() as $g ) {
                                $result['generator'][ApiBase::PARAM_TYPE][] = $g;
                                $result['generator'][ApiBase::PARAM_VALUE_LINKS][$g] = "Special:ApiHelp/query+$g";
                        }
index 55da52c..6d26a2d 100644 (file)
@@ -40,7 +40,9 @@ class ResourceFileCache extends FileCacheBase {
        public static function newFromContext( ResourceLoaderContext $context ) {
                $cache = new self();
 
-               if ( $context->getOnly() === 'styles' ) {
+               if ( $context->getImage() ) {
+                       $cache->mType = 'image';
+               } elseif ( $context->getOnly() === 'styles' ) {
                        $cache->mType = 'css';
                } else {
                        $cache->mType = 'js';
@@ -69,7 +71,8 @@ class ResourceFileCache extends FileCacheBase {
                // Get all query values
                $queryVals = $context->getRequest()->getValues();
                foreach ( $queryVals as $query => $val ) {
-                       if ( $query === 'modules' || $query === 'version' || $query === '*' ) {
+                       if ( in_array( $query, array( 'modules', 'image', 'variant', 'version', '*' ) ) ) {
+                               // Use file cache regardless of the value of this parameter
                                continue; // note: &* added as IE fix
                        } elseif ( $query === 'skin' && $val === $wgDefaultSkin ) {
                                continue;
@@ -79,6 +82,8 @@ class ResourceFileCache extends FileCacheBase {
                                continue;
                        } elseif ( $query === 'debug' && $val === 'false' ) {
                                continue;
+                       } elseif ( $query === 'format' && $val === 'rasterized' ) {
+                               continue;
                        }
 
                        return false;
index c82be50..ae6b659 100644 (file)
@@ -492,7 +492,7 @@ abstract class File {
                sort( $sortedBuckets );
 
                foreach ( $sortedBuckets as $bucket ) {
-                       if ( $bucket > $imageWidth ) {
+                       if ( $bucket >= $imageWidth ) {
                                return false;
                        }
 
index 5603aa5..a40c44a 100644 (file)
@@ -70,33 +70,12 @@ class ProfilerXhprof extends Profiler {
         */
        protected $sprofiler;
 
-       /**
-        * Type of report to send when logData() is called.
-        * @var string $logType
-        */
-       protected $logType;
-
-       /**
-        * Should profile report sent to in page content be visible?
-        * @var bool $visible
-        */
-       protected $visible;
-
        /**
         * @param array $params
         * @see Xhprof::__construct()
         */
        public function __construct( array $params = array() ) {
-               $params = array_merge(
-                       array(
-                               'log' => 'text',
-                               'visible' => false
-                       ),
-                       $params
-               );
                parent::__construct( $params );
-               $this->logType = $params['log'];
-               $this->visible = $params['visible'];
                $this->xhprof = new Xhprof( $params );
                $this->sprofiler = new SectionProfiler();
        }
index 3f4e172..14a6f30 100644 (file)
@@ -63,8 +63,11 @@ class ResourceLoader {
         */
        protected $sources = array();
 
-       /** @var bool */
-       protected $hasErrors = false;
+       /**
+        * Errors accumulated during current respond() call.
+        * @var array
+        */
+       protected $errors = array();
 
        /**
         * Load information stored in the database about modules.
@@ -209,9 +212,7 @@ class ResourceLoader {
                } catch ( Exception $e ) {
                        MWExceptionHandler::logException( $e );
                        wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
-                       $this->hasErrors = true;
-                       // Return exception as a comment
-                       $result = self::formatException( $e );
+                       $this->errors[] = self::formatExceptionNoComment( $e );
                }
 
                wfProfileOut( __METHOD__ );
@@ -579,7 +580,6 @@ class ResourceLoader {
                ob_start();
 
                wfProfileIn( __METHOD__ );
-               $errors = '';
 
                // Find out which modules are missing and instantiate the others
                $modules = array();
@@ -591,10 +591,7 @@ class ResourceLoader {
                                // This is a security issue, see bug 34907.
                                if ( $module->getGroup() === 'private' ) {
                                        wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
-                                       $this->hasErrors = true;
-                                       // Add exception to the output as a comment
-                                       $errors .= self::makeComment( "Cannot show private module \"$name\"" );
-
+                                       $this->errors[] = "Cannot show private module \"$name\"";
                                        continue;
                                }
                                $modules[$name] = $module;
@@ -609,9 +606,7 @@ class ResourceLoader {
                } catch ( Exception $e ) {
                        MWExceptionHandler::logException( $e );
                        wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
-                       $this->hasErrors = true;
-                       // Add exception to the output as a comment
-                       $errors .= self::formatException( $e );
+                       $this->errors[] = self::formatExceptionNoComment( $e );
                }
 
                wfProfileIn( __METHOD__ . '-getModifiedTime' );
@@ -629,9 +624,7 @@ class ResourceLoader {
                        } catch ( Exception $e ) {
                                MWExceptionHandler::logException( $e );
                                wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
-                               $this->hasErrors = true;
-                               // Add exception to the output as a comment
-                               $errors .= self::formatException( $e );
+                               $this->errors[] = self::formatExceptionNoComment( $e );
                        }
                }
 
@@ -646,19 +639,15 @@ class ResourceLoader {
                // Generate a response
                $response = $this->makeModuleResponse( $context, $modules, $missing );
 
-               // Prepend comments indicating exceptions
-               $response = $errors . $response;
-
                // Capture any PHP warnings from the output buffer and append them to the
-               // response in a comment if we're in debug mode.
+               // error list if we're in debug mode.
                if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
-                       $response = self::makeComment( $warnings ) . $response;
-                       $this->hasErrors = true;
+                       $this->errors[] = $warnings;
                }
 
                // Save response to file cache unless there are errors
-               if ( isset( $fileCache ) && !$errors && !count( $missing ) ) {
-                       // Cache single modules...and other requests if there are enough hits
+               if ( isset( $fileCache ) && !$this->errors && !count( $missing ) ) {
+                       // Cache single modules and images...and other requests if there are enough hits
                        if ( ResourceFileCache::useFileCache( $context ) ) {
                                if ( $fileCache->isCacheWorthy() ) {
                                        $fileCache->saveText( $response );
@@ -669,10 +658,28 @@ class ResourceLoader {
                }
 
                // Send content type and cache related headers
-               $this->sendResponseHeaders( $context, $mtime, $this->hasErrors );
+               $this->sendResponseHeaders( $context, $mtime, (bool)$this->errors );
 
                // Remove the output buffer and output the response
                ob_end_clean();
+
+               if ( $context->getImageObj() && $this->errors ) {
+                       // We can't show both the error messages and the response when it's an image.
+                       $errorText = '';
+                       foreach ( $this->errors as $error ) {
+                               $errorText .= $error . "\n";
+                       }
+                       $response = $errorText;
+               } elseif ( $this->errors ) {
+                       // Prepend comments indicating errors
+                       $errorText = '';
+                       foreach ( $this->errors as $error ) {
+                               $errorText .= self::makeComment( $error );
+                       }
+                       $response = $errorText . $response;
+               }
+
+               $this->errors = array();
                echo $response;
 
                wfProfileOut( __METHOD__ );
@@ -682,7 +689,7 @@ class ResourceLoader {
         * Send content type and last modified headers to the client.
         * @param ResourceLoaderContext $context
         * @param string $mtime TS_MW timestamp to use for last-modified
-        * @param bool $errors Whether there are commented-out errors in the response
+        * @param bool $errors Whether there are errors in the response
         * @return void
         */
        protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
@@ -699,7 +706,14 @@ class ResourceLoader {
                        $maxage = $rlMaxage['versioned']['client'];
                        $smaxage = $rlMaxage['versioned']['server'];
                }
-               if ( $context->getOnly() === 'styles' ) {
+               if ( $context->getImageObj() ) {
+                       // Output different headers if we're outputting textual errors.
+                       if ( $errors ) {
+                               header( 'Content-Type: text/plain; charset=utf-8' );
+                       } else {
+                               $context->getImageObj()->sendResponseHeaders( $context );
+                       }
+               } elseif ( $context->getOnly() === 'styles' ) {
                        header( 'Content-Type: text/css; charset=utf-8' );
                        header( 'Access-Control-Allow-Origin: *' );
                } else {
@@ -823,15 +837,26 @@ class ResourceLoader {
         * Handle exception display.
         *
         * @param Exception $e Exception to be shown to the user
-        * @return string Sanitized text that can be returned to the user
+        * @return string Sanitized text in a CSS/JS comment that can be returned to the user
         */
        public static function formatException( $e ) {
+               return self::makeComment( self::formatExceptionNoComment( $e ) );
+       }
+
+       /**
+        * Handle exception display.
+        *
+        * @since 1.25
+        * @param Exception $e Exception to be shown to the user
+        * @return string Sanitized text that can be returned to the user
+        */
+       protected static function formatExceptionNoComment( $e ) {
                global $wgShowExceptionDetails;
 
                if ( $wgShowExceptionDetails ) {
-                       return self::makeComment( $e->__toString() );
+                       return $e->__toString();
                } else {
-                       return self::makeComment( wfMessage( 'internalerror' )->text() );
+                       return wfMessage( 'internalerror' )->text();
                }
        }
 
@@ -847,7 +872,6 @@ class ResourceLoader {
                array $modules, array $missing = array()
        ) {
                $out = '';
-               $exceptions = '';
                $states = array();
 
                if ( !count( $modules ) && !count( $missing ) ) {
@@ -858,6 +882,17 @@ class ResourceLoader {
 
                wfProfileIn( __METHOD__ );
 
+               $image = $context->getImageObj();
+               if ( $image ) {
+                       $data = $image->getImageData( $context );
+                       if ( $data === false ) {
+                               $data = '';
+                               $this->errors[] = 'Image generation failed';
+                       }
+                       wfProfileOut( __METHOD__ );
+                       return $data;
+               }
+
                // Pre-fetch blobs
                if ( $context->shouldIncludeMessages() ) {
                        try {
@@ -868,9 +903,7 @@ class ResourceLoader {
                                        'resourceloader',
                                        __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e"
                                );
-                               $this->hasErrors = true;
-                               // Add exception to the output as a comment
-                               $exceptions .= self::formatException( $e );
+                               $this->errors[] = self::formatExceptionNoComment( $e );
                        }
                } else {
                        $blobs = array();
@@ -994,9 +1027,7 @@ class ResourceLoader {
                        } catch ( Exception $e ) {
                                MWExceptionHandler::logException( $e );
                                wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
-                               $this->hasErrors = true;
-                               // Add exception to the output as a comment
-                               $exceptions .= self::formatException( $e );
+                               $this->errors[] = self::formatExceptionNoComment( $e );
 
                                // Respond to client with error-state instead of module implementation
                                $states[$name] = 'error';
@@ -1022,9 +1053,8 @@ class ResourceLoader {
                        }
                } else {
                        if ( count( $states ) ) {
-                               $exceptions .= self::makeComment(
-                                       'Problematic modules: ' . FormatJson::encode( $states, ResourceLoader::inDebugMode() )
-                               );
+                               $this->errors[] = 'Problematic modules: ' .
+                                       FormatJson::encode( $states, ResourceLoader::inDebugMode() );
                        }
                }
 
@@ -1037,7 +1067,7 @@ class ResourceLoader {
                }
 
                wfProfileOut( __METHOD__ );
-               return $exceptions . $out;
+               return $out;
        }
 
        /* Static Methods */
index 02744a6..a6a7d34 100644 (file)
@@ -41,7 +41,11 @@ class ResourceLoaderContext {
        protected $version;
        protected $hash;
        protected $raw;
+       protected $image;
+       protected $variant;
+       protected $format;
        protected $userObj;
+       protected $imageObj;
 
        /* Methods */
 
@@ -66,6 +70,10 @@ class ResourceLoaderContext {
                $this->only = $request->getVal( 'only' );
                $this->version = $request->getVal( 'version' );
                $this->raw = $request->getFuzzyBool( 'raw' );
+               // Image requests
+               $this->image = $request->getVal( 'image' );
+               $this->variant = $request->getVal( 'variant' );
+               $this->format = $request->getVal( 'format' );
 
                $skinnames = Skin::getSkinNames();
                // If no skin is specified, or we don't recognize the skin, use the default skin
@@ -232,6 +240,62 @@ class ResourceLoaderContext {
                return $this->raw;
        }
 
+       /**
+        * @return string|null
+        */
+       public function getImage() {
+               return $this->image;
+       }
+
+       /**
+        * @return string|null
+        */
+       public function getVariant() {
+               return $this->variant;
+       }
+
+       /**
+        * @return string|null
+        */
+       public function getFormat() {
+               return $this->format;
+       }
+
+       /**
+        * If this is a request for an image, get the ResourceLoaderImage object.
+        *
+        * @since 1.25
+        * @return ResourceLoaderImage|bool false if a valid object cannot be created
+        */
+       public function getImageObj() {
+               if ( $this->imageObj === null ) {
+                       $this->imageObj = false;
+
+                       if ( !$this->image ) {
+                               return $this->imageObj;
+                       }
+
+                       $modules = $this->getModules();
+                       if ( count( $modules ) !== 1 ) {
+                               return $this->imageObj;
+                       }
+
+                       $module = $this->getResourceLoader()->getModule( $modules[0] );
+                       if ( !$module || !$module instanceof ResourceLoaderImageModule ) {
+                               return $this->imageObj;
+                       }
+
+                       $image = $module->getImage( $this->image );
+                       if ( !$image ) {
+                               return $this->imageObj;
+                       }
+
+                       $this->imageObj = $image;
+               }
+
+               return $this->imageObj;
+       }
+
        /**
         * @return bool
         */
@@ -260,6 +324,7 @@ class ResourceLoaderContext {
                if ( !isset( $this->hash ) ) {
                        $this->hash = implode( '|', array(
                                $this->getLanguage(), $this->getDirection(), $this->getSkin(), $this->getUser(),
+                               $this->getImage(), $this->getVariant(), $this->getFormat(),
                                $this->getDebug(), $this->getOnly(), $this->getVersion()
                        ) );
                }
diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php
new file mode 100644 (file)
index 0000000..0e43f65
--- /dev/null
@@ -0,0 +1,356 @@
+<?php
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+ *
+ * @file
+ */
+
+/**
+ * Class encapsulating an image used in a ResourceLoaderImageModule.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImage {
+
+       /**
+        * Map of allowed file extensions to their MIME types.
+        * @var array
+        */
+       protected static $fileTypes = array(
+               'svg' => 'image/svg+xml',
+               'png' => 'image/png',
+               'gif' => 'image/gif',
+               'jpg' => 'image/jpg',
+       );
+
+       /**
+        * @param string $name Image name
+        * @param string $module Module name
+        * @param string|array $descriptor Path to image file, or array structure containing paths
+        * @param string $basePath Directory to which paths in descriptor refer
+        * @param array $variants
+        * @throws MWException
+        */
+       public function __construct( $name, $module, $descriptor, $basePath, $variants ) {
+               $this->name = $name;
+               $this->module = $module;
+               $this->descriptor = $descriptor;
+               $this->basePath = $basePath;
+               $this->variants = $variants;
+
+               // Ensure that all files have common extension.
+               $extensions = array();
+               $descriptor = (array)$descriptor;
+               array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
+                       $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+               } );
+               $extensions = array_unique( $extensions );
+               if ( count( $extensions ) !== 1 ) {
+                       throw new MWException( 'Image type for various images differs.' );
+               }
+               $ext = $extensions[0];
+               if ( !isset( self::$fileTypes[$ext] ) ) {
+                       throw new MWException( 'Invalid image type; svg, png, gif or jpg required.' );
+               }
+               $this->extension = $ext;
+       }
+
+       /**
+        * Get name of this image.
+        *
+        * @return string
+        */
+       public function getName() {
+               return $this->name;
+       }
+
+       /**
+        * Get name of the module this image belongs to.
+        *
+        * @return string
+        */
+       public function getModule() {
+               return $this->module;
+       }
+
+       /**
+        * Get the list of variants this image can be converted to.
+        *
+        * @return string[]
+        */
+       public function getVariants() {
+               return array_keys( $this->variants );
+       }
+
+       /**
+        * Get the path to image file for given context.
+        *
+        * @param ResourceLoaderContext $context Any context
+        * @return string
+        */
+       protected function getPath( ResourceLoaderContext $context ) {
+               $desc = $this->descriptor;
+               if ( is_string( $desc ) ) {
+                       return $this->basePath . '/' . $desc;
+               } elseif ( isset( $desc['lang'][ $context->getLanguage() ] ) ) {
+                       return $this->basePath . '/' . $desc['lang'][ $context->getLanguage() ];
+               } elseif ( isset( $desc[ $context->getDirection() ] ) ) {
+                       return $this->basePath . '/' . $desc[ $context->getDirection() ];
+               } else {
+                       return $this->basePath . '/' . $desc['default'];
+               }
+       }
+
+       /**
+        * Get the extension of the image.
+        *
+        * @param string $format Format to get the extension for, 'original' or 'rasterized'
+        * @return string Extension without leading dot, e.g. 'png'
+        */
+       public function getExtension( $format = 'original' ) {
+               if ( $format === 'rasterized' && $this->extension === 'svg' ) {
+                       return 'png';
+               } else {
+                       return $this->extension;
+               }
+       }
+
+       /**
+        * Get the MIME type of the image.
+        *
+        * @param string $format Format to get the MIME type for, 'original' or 'rasterized'
+        * @return string
+        */
+       public function getMimeType( $format = 'original' ) {
+               $ext = $this->getExtension( $format );
+               return self::$fileTypes[$ext];
+       }
+
+       /**
+        * Get the load.php URL that will produce this image.
+        *
+        * @param ResourceLoaderContext $context Any context
+        * @param string $script URL to load.php
+        * @param string|null $variant Variant to get the URL for
+        * @param string $format Format to get the URL for, 'original' or 'rasterized'
+        * @return string
+        */
+       public function getUrl( ResourceLoaderContext $context, $script, $variant, $format ) {
+               $query = array(
+                       'modules' => $this->getModule(),
+                       'image' => $this->getName(),
+                       'variant' => $variant,
+                       'format' => $format,
+                       'lang' => $context->getLanguage(),
+                       'version' => $context->getVersion(),
+               );
+
+               return wfExpandUrl( wfAppendQuery( $script, $query ), PROTO_RELATIVE );
+       }
+
+       /**
+        * Get the data: URI that will produce this image.
+        *
+        * @param ResourceLoaderContext $context Any context
+        * @param string|null $variant Variant to get the URI for
+        * @param string $format Format to get the URI for, 'original' or 'rasterized'
+        * @return string
+        */
+       public function getDataUri( ResourceLoaderContext $context, $variant, $format ) {
+               $type = $this->getMimeType( $format );
+               $contents = $this->getImageData( $context, $variant, $format );
+               return CSSMin::encodeStringAsDataURI( $contents, $type );
+       }
+
+       /**
+        * Get actual image data for this image. This can be saved to a file or sent to the browser to
+        * produce the converted image.
+        *
+        * Call getExtension() or getMimeType() with the same $format argument to learn what file type the
+        * returned data uses.
+        *
+        * @param ResourceLoaderContext $context Image context, or any context of $variant and $format
+        *     given.
+        * @param string|null $variant Variant to get the data for. Optional, if given, overrides the data
+        *     from $context.
+        * @param string $format Format to get the data for, 'original' or 'rasterized'. Optional, if
+        *     given, overrides the data from $context.
+        * @return string|false Possibly binary image data, or false on failure
+        */
+       public function getImageData( ResourceLoaderContext $context, $variant = false, $format = false ) {
+               if ( $variant === false ) {
+                       $variant = $context->getVariant();
+               }
+               if ( $format === false ) {
+                       $format = $context->getFormat();
+               }
+
+               if ( $this->getExtension() !== 'svg' ) {
+                       return file_get_contents( $this->getPath( $context ) );
+               }
+
+               if ( $variant && isset( $this->variants[$variant] ) ) {
+                       $data = $this->variantize( $this->variants[$variant], $context );
+               } else {
+                       $data = file_get_contents( $this->getPath( $context ) );
+               }
+
+               if ( $format === 'rasterized' ) {
+                       $data = $this->rasterize( $data );
+               }
+
+               return $data;
+       }
+
+       /**
+        * Send response headers (using the header() function) that are necessary to correctly serve the
+        * image data for this image, as returned by getImageData().
+        *
+        * Note that the headers are independent of the language or image variant.
+        *
+        * @param ResourceLoaderContext $context Image context
+        */
+       public function sendResponseHeaders( ResourceLoaderContext $context ) {
+               $format = $context->getFormat();
+               $mime = $this->getMimeType( $format );
+               $filename = $this->getName() . '.' . $this->getExtension( $format );
+
+               header( 'Content-Type: ' . $mime );
+               header( 'Content-Disposition: ' .
+                       FileBackend::makeContentDisposition( 'inline', $filename ) );
+       }
+
+       /**
+        * Convert this image, which is assumed to be SVG, to given variant.
+        *
+        * @param array $variantConf Array with a 'color' key, its value will be used as fill color
+        * @param ResourceLoaderContext $context Image context
+        * @return string New SVG file data
+        */
+       protected function variantize( $variantConf, ResourceLoaderContext $context ) {
+               $dom = new DomDocument;
+               $dom->load( $this->getPath( $context ) );
+               $root = $dom->documentElement;
+               $wrapper = $dom->createElement( 'g' );
+               while ( $root->firstChild ) {
+                       $wrapper->appendChild( $root->firstChild );
+               }
+               $root->appendChild( $wrapper );
+               $wrapper->setAttribute( 'fill', $variantConf['color'] );
+               return $dom->saveXml();
+       }
+
+       /**
+        * Massage the SVG image data for converters which doesn't understand some path data syntax.
+        *
+        * This is necessary for rsvg and ImageMagick when compiled with rsvg support.
+        * Upstream bug is https://bugzilla.gnome.org/show_bug.cgi?id=620923, fixed 2014-11-10, so
+        * this will be needed for a while. (T76852)
+        *
+        * @param string $svg SVG image data
+        * @return string Massaged SVG image data
+        */
+       protected function massageSvgPathdata( $svg ) {
+               $dom = new DomDocument;
+               $dom->loadXml( $svg );
+               foreach ( $dom->getElementsByTagName( 'path' ) as $node ) {
+                       $pathData = $node->getAttribute( 'd' );
+                       // Make sure there is at least one space between numbers, and that leading zero is not omitted.
+                       // rsvg has issues with syntax like "M-1-2" and "M.445.483" and especially "M-.445-.483".
+                       $pathData = preg_replace( '/(-?)(\d*\.\d+|\d+)/', ' ${1}0$2 ', $pathData );
+                       // Strip unnecessary leading zeroes for prettiness, not strictly necessary
+                       $pathData = preg_replace( '/([ -])0(\d)/', '$1$2', $pathData );
+                       $node->setAttribute( 'd', $pathData );
+               }
+               return $dom->saveXml();
+       }
+
+       /**
+        * Convert passed image data, which is assumed to be SVG, to PNG.
+        *
+        * @param string $svg SVG image data
+        * @return string|bool PNG image data, or false on failure
+        */
+       protected function rasterize( $svg ) {
+               // This code should be factored out to a separate method on SvgHandler, or perhaps a separate
+               // class, with a separate set of configuration settings.
+               //
+               // This is a distinct use case from regular SVG rasterization:
+               // * we can skip many sanity and security checks (as the images come from a trusted source,
+               //   rather than from the user)
+               // * we need to provide extra options to some converters to achieve acceptable quality for very
+               //   small images, which might cause performance issues in the general case
+               // * we need to directly pass image data to the converter instead of a file path
+               //
+               // See https://phabricator.wikimedia.org/T76473#801446 for examples of what happens with the
+               // default settings.
+               //
+               // For now, we special-case rsvg (used in WMF production) and do a messy workaround for other
+               // converters.
+
+               global $wgSVGConverter, $wgSVGConverterPath;
+
+               $svg = $this->massageSvgPathdata( $svg );
+
+               if ( $wgSVGConverter === 'rsvg' ) {
+                       $command = 'rsvg-convert'; // Should be just 'rsvg'? T76476
+                       if ( $wgSVGConverterPath ) {
+                               $command = wfEscapeShellArg( "$wgSVGConverterPath/" ) . $command;
+                       }
+
+                       $process = proc_open(
+                               $command,
+                               array( 0 => array( 'pipe', 'r' ), 1 => array( 'pipe', 'w' ) ),
+                               $pipes
+                       );
+
+                       if ( is_resource( $process ) ) {
+                               fwrite( $pipes[0], $svg );
+                               fclose( $pipes[0] );
+                               $png = stream_get_contents( $pipes[1] );
+                               fclose( $pipes[1] );
+                               proc_close( $process );
+
+                               return $png ?: false;
+                       }
+                       return false;
+
+               } else {
+                       // Write input to and read output from a temporary file
+                       $tempFilenameSvg = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+                       $tempFilenamePng = tempnam( wfTempDir(), 'ResourceLoaderImage' );
+
+                       file_put_contents( $tempFilenameSvg, $svg );
+
+                       $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg );
+                       if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) {
+                               return false;
+                       }
+
+                       $handler = new SvgHandler;
+                       $handler->rasterize( $tempFilenameSvg, $tempFilenamePng, $metadata['width'], $metadata['height'] );
+
+                       $png = file_get_contents( $tempFilenamePng );
+
+                       unlink( $tempFilenameSvg );
+                       unlink( $tempFilenamePng );
+
+                       return $png ?: false;
+               }
+       }
+}
diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php
new file mode 100644 (file)
index 0000000..2ea3c06
--- /dev/null
@@ -0,0 +1,294 @@
+<?php
+/**
+ * Resource loader module for generated and embedded images.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * 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
+ *
+ * @file
+ * @author Trevor Parscal
+ */
+
+/**
+ * Resource loader module for generated and embedded images.
+ *
+ * @since 1.25
+ */
+class ResourceLoaderImageModule extends ResourceLoaderModule {
+
+       /**
+        * Local base path, see __construct()
+        * @var string
+        */
+       protected $localBasePath = '';
+
+       protected $origin = self::ORIGIN_CORE_SITEWIDE;
+
+       protected $images = array();
+       protected $variants = array();
+       protected $prefix = array();
+
+       /**
+        * Constructs a new module from an options array.
+        *
+        * @param array $options List of options; if not given or empty, an empty module will be
+        *     constructed
+        * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
+        *     to $IP
+        *
+        * Below is a description for the $options array:
+        * @par Construction options:
+        * @code
+        *     array(
+        *         // Base path to prepend to all local paths in $options. Defaults to $IP
+        *         'localBasePath' => [base path],
+        *         // CSS class prefix to use in all style rules
+        *         'prefix' => [CSS class prefix],
+        *         // List of variants that may be used for the image files
+        *         'variants' => array(
+        *             // ([image type] is a string, used in generated CSS class names and to match variants to images)
+        *             [image type] => array(
+        *                 [variant name] => array(
+        *                     'color' => [color string, e.g. '#ffff00'],
+        *                     'global' => [boolean, if true, this variant is available for all images of this type],
+        *                 ),
+        *             )
+        *         ),
+        *         // List of image files and their options
+        *         'images' => array(
+        *             [image type] => array(
+        *                 [file path string],
+        *                 [file path string] => array(
+        *                     'name' => [image name string, defaults to file name],
+        *                     'variants' => [array of variant name strings, variants available for this image],
+        *                 ),
+        *             )
+        *         ),
+        *     )
+        * @endcode
+        * @throws MWException
+        */
+       public function __construct( $options = array(), $localBasePath = null ) {
+               $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
+
+               if ( !isset( $options['prefix'] ) || !$options['prefix'] ) {
+                       throw new MWException(
+                               "Required 'prefix' option not given or empty."
+                       );
+               }
+
+               foreach ( $options as $member => $option ) {
+                       switch ( $member ) {
+                               case 'images':
+                                       if ( !is_array( $option ) ) {
+                                               throw new MWException(
+                                                       "Invalid collated file path list error. '$option' given, array expected."
+                                               );
+                                       }
+                                       foreach ( $option as $key => $value ) {
+                                               if ( !is_string( $key ) ) {
+                                                       throw new MWException(
+                                                               "Invalid collated file path list key error. '$key' given, string expected."
+                                                       );
+                                               }
+                                               $this->{$member}[$key] = (array)$value;
+                                       }
+                                       break;
+
+                               case 'variants':
+                                       if ( !is_array( $option ) ) {
+                                               throw new MWException(
+                                                       "Invalid variant list error. '$option' given, array expected."
+                                               );
+                                       }
+                                       $this->{$member} = $option;
+                                       break;
+
+                               case 'prefix':
+                                       $this->{$member} = (string)$option;
+                                       break;
+                       }
+               }
+       }
+
+       /**
+        * Get CSS class prefix used by this module.
+        * @return string
+        */
+       public function getPrefix() {
+               return $this->prefix;
+       }
+
+       /**
+        * Get a ResourceLoaderImage object for given image.
+        * @param string $name Image name
+        * @return ResourceLoaderImage|null
+        */
+       public function getImage( $name ) {
+               $images = $this->getImages();
+               return isset( $images[$name] ) ? $images[$name] : null;
+       }
+
+       /**
+        * Get ResourceLoaderImage objects for all images.
+        * @return ResourceLoaderImage[] Array keyed by image name
+        */
+       public function getImages() {
+               if ( !isset( $this->imageObjects ) ) {
+                       $this->imageObjects = array();
+
+                       foreach ( $this->images as $type => $list ) {
+                               foreach ( $list as $name => $options ) {
+                                       $imageDesc = is_string( $options ) ? $options : $options['image'];
+
+                                       $allowedVariants = array_merge(
+                                               isset( $options['variants'] ) ? $options['variants'] : array(),
+                                               $this->getGlobalVariants( $type )
+                                       );
+                                       $variantConfig = array_intersect_key(
+                                               $this->variants[$type],
+                                               array_fill_keys( $allowedVariants, true )
+                                       );
+
+                                       $image = new ResourceLoaderImage( $name, $this->getName(), $imageDesc, $this->localBasePath, $variantConfig );
+                                       $this->imageObjects[ $image->getName() ] = $image;
+                               }
+                       }
+               }
+
+               return $this->imageObjects;
+       }
+
+       /**
+        * Get list of variants in this module that are 'global' for given type of images, i.e., available
+        * for every image of given type regardless of image options.
+        * @param string $type Image type
+        * @return string[]
+        */
+       public function getGlobalVariants( $type ) {
+               if ( !isset( $this->globalVariants[$type] ) ) {
+                       $this->globalVariants[$type] = array();
+
+                       foreach ( $this->variants[$type] as $name => $config ) {
+                               if ( isset( $config['global'] ) && $config['global'] ) {
+                                       $this->globalVariants[$type][] = $name;
+                               }
+                       }
+               }
+
+               return $this->globalVariants[$type];
+       }
+
+       /**
+        * Get the type of given image.
+        * @param string $imageName Image name
+        * @return string
+        */
+       public function getImageType( $imageName ) {
+               foreach ( $this->images as $type => $list ) {
+                       foreach ( $list as $key => $value ) {
+                               $file = is_int( $key ) ? $value : $key;
+                               $options = is_array( $value ) ? $value : array();
+                               $name = isset( $options['name'] ) ? $options['name'] : pathinfo( $file, PATHINFO_FILENAME );
+                               if ( $name === $imageName ) {
+                                       return $type;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * @param ResourceLoaderContext $context
+        * @return array
+        */
+       public function getStyles( ResourceLoaderContext $context ) {
+               // Build CSS rules
+               $rules = array();
+               $script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
+               $prefix = $this->getPrefix();
+
+               foreach ( $this->getImages() as $name => $image ) {
+                       $type = $this->getImageType( $name );
+
+                       $declarations = $this->getCssDeclarations(
+                               $image->getDataUri( $context, null, 'original' ),
+                               $image->getUrl( $context, $script, null, 'rasterized' )
+                       );
+                       $declarations = implode( "\n\t", $declarations );
+                       $rules[] = ".$prefix-$type-$name {\n\t$declarations\n}";
+
+                       // TODO: Get variant configurations from $context->getSkin()
+                       foreach ( $image->getVariants() as $variant ) {
+                               $declarations = $this->getCssDeclarations(
+                                       $image->getDataUri( $context, $variant, 'original' ),
+                                       $image->getUrl( $context, $script, $variant, 'rasterized' )
+                               );
+                               $declarations = implode( "\n\t", $declarations );
+                               $rules[] = ".$prefix-$type-$name-$variant {\n\t$declarations\n}";
+                       }
+               }
+
+               $style = implode( "\n", $rules );
+               if ( $this->getFlip( $context ) ) {
+                       $style = CSSJanus::transform( $style, true, false );
+               }
+               return array( 'all' => $style );
+       }
+
+       /**
+        * @param string $primary Primary URI
+        * @param string $fallback Fallback URI
+        * @return string[] CSS declarations to use given URIs as background-image
+        */
+       protected function getCssDeclarations( $primary, $fallback ) {
+               // SVG support using a transparent gradient to guarantee cross-browser
+               // compatibility (browsers able to understand gradient syntax support also SVG).
+               // http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique
+               return array(
+                       "background-image: url($fallback);",
+                       "background-image: -webkit-linear-gradient(transparent, transparent), url($primary);",
+                       "background-image: linear-gradient(transparent, transparent), url($primary);",
+               );
+       }
+
+       /**
+        * @return bool
+        */
+       public function supportsURLLoading() {
+               return false;
+       }
+
+       /**
+        * Extract a local base path from module definition information.
+        *
+        * @param array $options Module definition
+        * @param string $localBasePath Path to use if not provided in module definition. Defaults
+        *     to $IP
+        * @return string Local base path
+        */
+       public static function extractLocalBasePath( $options, $localBasePath = null ) {
+               global $IP;
+
+               if ( $localBasePath === null ) {
+                       $localBasePath = $IP;
+               }
+
+               if ( array_key_exists( 'localBasePath', $options ) ) {
+                       $localBasePath = (string)$options['localBasePath'];
+               }
+
+               return $localBasePath;
+       }
+}
index 66e8da0..d17b06d 100644 (file)
@@ -91,6 +91,7 @@ class FindHooks extends Maintenance {
                        $IP . '/includes/jobqueue/',
                        $IP . '/includes/json/',
                        $IP . '/includes/logging/',
+                       $IP . '/includes/mail/',
                        $IP . '/includes/media/',
                        $IP . '/includes/page/',
                        $IP . '/includes/parser/',
index 470647a..55f535d 100644 (file)
@@ -37,12 +37,18 @@ class UpdateArticleCount extends Maintenance {
                parent::__construct();
                $this->mDescription = "Count of the number of articles and update the site statistics table";
                $this->addOption( 'update', 'Update the site_stats table with the new count' );
+               $this->addOption( 'use-master', 'Count using the master database' );
        }
 
        public function execute() {
                $this->output( "Counting articles..." );
 
-               $counter = new SiteStatsInit( false );
+               if ( $this->hasOption( 'use-master' ) ) {
+                       $dbr = wfGetDB( DB_MASTER );
+               } else {
+                       $dbr = wfGetDB( DB_SLAVE, 'vslow' );
+               }
+               $counter = new SiteStatsInit( $dbr );
                $result = $counter->articles();
 
                $this->output( "found {$result}.\n" );
index 39129cb..648c495 100644 (file)
@@ -4099,6 +4099,31 @@ External links: with no contents
 <p><a rel="mw:ExtLink" href="http://en.wikipedia.org/wiki/Foo" title="wikipedia:Foo"><span>Bar</span></a></p>
 !! end
 
+!! test
+External links: Free with trailing punctuation
+!! wikitext
+http://example.com,
+http://example.com;
+http://example.com\
+http://example.com.
+http://example.com:
+http://example.com!
+http://example.com?
+http://example.com)
+http://example.com/url_with_(brackets)
+!! html
+<p><a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>,
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>;
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>\
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>.
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>:
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>!
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>?
+<a rel="nofollow" class="external free" href="http://example.com">http://example.com</a>)
+<a rel="nofollow" class="external free" href="http://example.com/url_with_(brackets)">http://example.com/url_with_(brackets)</a>
+</p>
+!! end
+
 !! test
 External image
 !! wikitext