Merge "Fix all the Doxygen for the RCFilters backend"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 4 May 2017 08:46:35 +0000 (08:46 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 4 May 2017 08:46:35 +0000 (08:46 +0000)
18 files changed:
RELEASE-NOTES-1.30
autoload.php
includes/DefaultSettings.php
includes/GlobalFunctions.php
includes/installer/Installer.php
includes/installer/LocalSettingsGenerator.php
includes/media/ExifBitmap.php
includes/media/Jpeg.php
includes/specialpage/ChangesListSpecialPage.php
includes/specials/SpecialRecentchanges.php
maintenance/mwdoc-filter.php
maintenance/mwdocgen.php
tests/phpunit/data/media/adobergb.jpg [new file with mode: 0644]
tests/phpunit/data/media/missingprofile.jpg [new file with mode: 0644]
tests/phpunit/data/media/srgb.jpg
tests/phpunit/data/media/tinyrgb.jpg
tests/phpunit/includes/media/ExifBitmapTest.php
tests/phpunit/includes/media/JpegTest.php

index 8b6a932..cdf8ba4 100644 (file)
@@ -6,7 +6,12 @@ MediaWiki 1.30 is an alpha-quality branch and is not recommended for use in
 production.
 
 === Configuration changes in 1.30 ===
-* …
+* The C.UTF-8 locale should be used for $wgShellLocale, if available, to avoid
+  unexpected behavior when things use local-sensitive string comparisons. For
+  example, Scribunto considers "bar" < "Foo" in most locales since it ignores
+  case.
+* $wgShellLocale now affects LC_ALL rather than only LC_CTYPE. See
+  documentation of $wgShellLocale for details.
 
 === New features in 1.30 ===
 * …
index 06e863f..1141c39 100644 (file)
@@ -786,7 +786,6 @@ $wgAutoloadLocalClasses = [
        'MWCryptRand' => __DIR__ . '/includes/utils/MWCryptRand.php',
        'MWDebug' => __DIR__ . '/includes/debug/MWDebug.php',
        'MWDocGen' => __DIR__ . '/maintenance/mwdocgen.php',
-       'MWDocGenFilter' => __DIR__ . '/maintenance/mwdoc-filter.php',
        'MWException' => __DIR__ . '/includes/exception/MWException.php',
        'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php',
        'MWExceptionRenderer' => __DIR__ . '/includes/exception/MWExceptionRenderer.php',
index ac2261c..7c18fcc 100644 (file)
@@ -8162,11 +8162,41 @@ $wgShellCgroup = false;
 $wgPhpCli = '/usr/bin/php';
 
 /**
- * Locale for LC_CTYPE, to work around https://bugs.php.net/bug.php?id=45132
- * For Unix-like operating systems, set this to to a locale that has a UTF-8
- * character set. Only the character set is relevant.
- */
-$wgShellLocale = 'en_US.utf8';
+ * Locale for LC_ALL, to provide a known environment for locale-sensitive operations
+ *
+ * For Unix-like operating systems, this should be set to C.UTF-8 or an
+ * equivalent to provide the most consistent behavior for locale-sensitive
+ * C library operations across different-language wikis. If that locale is not
+ * available, use another locale that has a UTF-8 character set.
+ *
+ * This setting mainly affects the behavior of C library functions, including:
+ *  - String collation (order when sorting using locale-sensitive comparison)
+ *    - For example, whether "Å" and "A" are considered to be the same letter or
+ *      different letters and if different whether it comes after "A" or after
+ *      "Z", and whether sorting is case sensitive.
+ *  - String character set (how characters beyond basic ASCII are represented)
+ *    - We need this to be a UTF-8 character set to work around
+ *      https://bugs.php.net/bug.php?id=45132
+ *  - Language used for low-level error messages.
+ *  - Formatting of date/time and numeric values (e.g. '.' versus ',' as the
+ *    decimal separator)
+ *
+ * MediaWiki provides its own methods and classes to perform many
+ * locale-sensitive operations, which are designed to be able to vary locale
+ * based on wiki language or user preference:
+ *  - MediaWiki's Collation class should generally be used instead of the C
+ *    library collation functions when locale-sensitive sorting is needed.
+ *  - MediaWiki's Message class should be used for localization of messages
+ *    displayed to the user.
+ *  - MediaWiki's Language class should be used for formatting numeric and
+ *    date/time values.
+ *
+ * @note If multiple wikis are being served from the same process (e.g. the
+ *  same fastCGI or Apache server), this setting must be the same on all those
+ *  wikis.
+ * @see wfInitShellLocale()
+ */
+$wgShellLocale = 'C.UTF-8';
 
 /** @} */ # End shell }
 
index 4325328..c7f132a 100644 (file)
@@ -2535,8 +2535,15 @@ function wfShellExecWithStderr( $cmd, &$retval = null, $environ = [], $limits =
 }
 
 /**
- * Workaround for https://bugs.php.net/bug.php?id=45132
- * escapeshellarg() destroys non-ASCII characters if LANG is not a UTF-8 locale
+ * Set the locale for locale-sensitive operations
+ *
+ * Sets LC_ALL to a known value to work around issues like the following:
+ * - https://bugs.php.net/bug.php?id=45132 escapeshellarg() destroys non-ASCII
+ *   characters if LANG is not a UTF-8 locale
+ * - T107128 Scribunto string comparison works case insensitive while the
+ *   standard Lua case sensitive
+ *
+ * @see $wgShellLocale
  */
 function wfInitShellLocale() {
        static $done = false;
@@ -2545,8 +2552,8 @@ function wfInitShellLocale() {
        }
        $done = true;
        global $wgShellLocale;
-       putenv( "LC_CTYPE=$wgShellLocale" );
-       setlocale( LC_CTYPE, $wgShellLocale );
+       putenv( "LC_ALL=$wgShellLocale" );
+       setlocale( LC_ALL, $wgShellLocale );
 }
 
 /**
index 12e8dd1..7028224 100644 (file)
@@ -1016,7 +1016,7 @@ abstract class Installer {
                }
 
                # Try the most common ones.
-               $commonLocales = [ 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ];
+               $commonLocales = [ 'C.UTF-8', 'en_US.UTF-8', 'en_US.utf8', 'de_DE.UTF-8', 'de_DE.utf8' ];
                foreach ( $commonLocales as $commonLocale ) {
                        if ( isset( $candidatesByLocale[$commonLocale] ) ) {
                                $this->setVar( 'wgShellLocale', $commonLocale );
index 697188e..7df1009 100644 (file)
@@ -241,7 +241,7 @@ class LocalSettingsGenerator {
                }
 
                if ( !$this->values['wgShellLocale'] ) {
-                       $this->values['wgShellLocale'] = 'en_US.UTF-8';
+                       $this->values['wgShellLocale'] = 'C.UTF-8';
                        $locale = '#';
                } else {
                        $locale = '';
index 7aeefa0..0e10abb 100644 (file)
@@ -30,7 +30,6 @@
 class ExifBitmapHandler extends BitmapHandler {
        const BROKEN_FILE = '-1'; // error extracting metadata
        const OLD_BROKEN_FILE = '0'; // outdated error extracting metadata.
-       const SRGB_ICC_PROFILE_NAME = 'IEC 61966-2.1 Default RGB colour space - sRGB';
 
        function convertMetadataVersion( $metadata, $version = 1 ) {
                // basically flattens arrays.
@@ -243,75 +242,4 @@ class ExifBitmapHandler extends BitmapHandler {
 
                return 0;
        }
-
-       protected function transformImageMagick( $image, $params ) {
-               global $wgUseTinyRGBForJPGThumbnails;
-
-               $ret = parent::transformImageMagick( $image, $params );
-
-               if ( $ret ) {
-                       return $ret;
-               }
-
-               if ( $params['mimeType'] === 'image/jpeg' && $wgUseTinyRGBForJPGThumbnails ) {
-                       // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
-                       // (and free) TinyRGB
-
-                       $this->swapICCProfile(
-                               $params['dstPath'],
-                               self::SRGB_ICC_PROFILE_NAME,
-                               realpath( __DIR__ ) . '/tinyrgb.icc'
-                       );
-               }
-
-               return false;
-       }
-
-       /**
-        * Swaps an embedded ICC profile for another, if found.
-        * Depends on exiftool, no-op if not installed.
-        * @param string $filepath File to be manipulated (will be overwritten)
-        * @param string $oldProfileString Exact name of color profile to look for
-        *  (the one that will be replaced)
-        * @param string $profileFilepath ICC profile file to apply to the file
-        * @since 1.26
-        * @return bool
-        */
-       public function swapICCProfile( $filepath, $oldProfileString, $profileFilepath ) {
-               global $wgExiftool;
-
-               if ( !$wgExiftool || !is_executable( $wgExiftool ) ) {
-                       return false;
-               }
-
-               $cmd = wfEscapeShellArg( $wgExiftool,
-                       '-DeviceModelDesc',
-                       '-S',
-                       '-T',
-                       $filepath
-               );
-
-               $output = wfShellExecWithStderr( $cmd, $retval );
-
-               if ( $retval !== 0 || strcasecmp( trim( $output ), $oldProfileString ) !== 0 ) {
-                       // We can't establish that this file has the expected ICC profile, don't process it
-                       return false;
-               }
-
-               $cmd = wfEscapeShellArg( $wgExiftool,
-                       '-overwrite_original',
-                       '-icc_profile<=' . $profileFilepath,
-                       $filepath
-               );
-
-               $output = wfShellExecWithStderr( $cmd, $retval );
-
-               if ( $retval !== 0 ) {
-                       $this->logErrorForExternalProcess( $retval, $output, $cmd );
-
-                       return false;
-               }
-
-               return true;
-       }
 }
index c9f0dfa..5822699 100644 (file)
@@ -31,6 +31,8 @@
  * @ingroup Media
  */
 class JpegHandler extends ExifBitmapHandler {
+       const SRGB_EXIF_COLOR_SPACE = 'sRGB';
+       const SRGB_ICC_PROFILE_DESCRIPTION = 'sRGB IEC61966-2.1';
 
        function normaliseParams( $image, &$params ) {
                if ( !parent::normaliseParams( $image, $params ) ) {
@@ -171,4 +173,118 @@ class JpegHandler extends ExifBitmapHandler {
 
                return $params;
        }
+
+       /**
+        * {@inheritdoc}
+        */
+       protected function transformImageMagick( $image, $params ) {
+               global $wgUseTinyRGBForJPGThumbnails;
+
+               $ret = parent::transformImageMagick( $image, $params );
+
+               if ( $ret ) {
+                       return $ret;
+               }
+
+               if ( $wgUseTinyRGBForJPGThumbnails ) {
+                       // T100976 If the profile embedded in the JPG is sRGB, swap it for the smaller
+                       // (and free) TinyRGB
+
+                       /**
+                        * We'll want to replace the color profile for JPGs:
+                        * * in the sRGB color space, or with the sRGB profile
+                        *   (other profiles will be left untouched)
+                        * * without color space or profile, in which case browsers
+                        *   should assume sRGB, but don't always do (e.g. on wide-gamut
+                        *   monitors (unless it's meant for low bandwith)
+                        * @see https://phabricator.wikimedia.org/T134498
+                        */
+                       $colorSpaces = [ self::SRGB_EXIF_COLOR_SPACE, '-' ];
+                       $profiles = [ self::SRGB_ICC_PROFILE_DESCRIPTION ];
+
+                       // we'll also add TinyRGB profile to images lacking a profile, but
+                       // only if they're not low quality (which are meant to save bandwith
+                       // and we don't want to increase the filesize by adding a profile)
+                       if ( $params['quality'] > 30 ) {
+                               $profiles[] = '-';
+                       }
+
+                       $this->swapICCProfile(
+                               $params['dstPath'],
+                               $colorSpaces,
+                               $profiles,
+                               realpath( __DIR__ ) . '/tinyrgb.icc'
+                       );
+               }
+
+               return false;
+       }
+
+       /**
+        * Swaps an embedded ICC profile for another, if found.
+        * Depends on exiftool, no-op if not installed.
+        * @param string $filepath File to be manipulated (will be overwritten)
+        * @param array $colorSpaces Only process files with this/these Color Space(s)
+        * @param array $oldProfileStrings Exact name(s) of color profile to look for
+        *  (the one that will be replaced)
+        * @param string $profileFilepath ICC profile file to apply to the file
+        * @since 1.26
+        * @return bool
+        */
+       public function swapICCProfile( $filepath, array $colorSpaces,
+                                                                       array $oldProfileStrings, $profileFilepath
+       ) {
+               global $wgExiftool;
+
+               if ( !$wgExiftool || !is_executable( $wgExiftool ) ) {
+                       return false;
+               }
+
+               $cmd = wfEscapeShellArg( $wgExiftool,
+                       '-EXIF:ColorSpace',
+                       '-ICC_Profile:ProfileDescription',
+                       '-S',
+                       '-T',
+                       $filepath
+               );
+
+               $output = wfShellExecWithStderr( $cmd, $retval );
+
+               // Explode EXIF data into an array with [0 => Color Space, 1 => Device Model Desc]
+               $data = explode( "\t", trim( $output ) );
+
+               if ( $retval !== 0 ) {
+                       return false;
+               }
+
+               // Make a regex out of the source data to match it to an array of color
+               // spaces in a case-insensitive way
+               $colorSpaceRegex = '/'.preg_quote( $data[0], '/' ).'/i';
+               if ( empty( preg_grep( $colorSpaceRegex, $colorSpaces ) ) ) {
+                       // We can't establish that this file matches the color space, don't process it
+                       return false;
+               }
+
+               $profileRegex = '/'.preg_quote( $data[1], '/' ).'/i';
+               if ( empty( preg_grep( $profileRegex, $oldProfileStrings ) ) ) {
+                       // We can't establish that this file has the expected ICC profile, don't process it
+                       return false;
+               }
+
+               $cmd = wfEscapeShellArg( $wgExiftool,
+                       '-overwrite_original',
+                       '-icc_profile<=' . $profileFilepath,
+                       $filepath
+               );
+
+               $output = wfShellExecWithStderr( $cmd, $retval );
+
+               if ( $retval !== 0 ) {
+                       $this->logErrorForExternalProcess( $retval, $output, $cmd );
+
+                       return false;
+               }
+
+               return true;
+       }
 }
index 87d73fc..97a0f50 100644 (file)
@@ -338,6 +338,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                'name' => 'changeType',
                                'title' => 'rcfilters-filtergroup-changetype',
                                'class' => ChangesListBooleanFilterGroup::class,
+                               'priority' => -8,
                                'filters' => [
                                        [
                                                'name' => 'hidepageedits',
@@ -393,96 +394,6 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                                ],
                        ],
 
-                       [
-                               'name' => 'watchlist',
-                               'title' => 'rcfilters-filtergroup-watchlist',
-                               'class' => ChangesListStringOptionsFilterGroup::class,
-                               'isFullCoverage' => true,
-                               'filters' => [
-                                       [
-                                               'name' => 'watched',
-                                               'label' => 'rcfilters-filter-watchlist-watched-label',
-                                               'description' => 'rcfilters-filter-watchlist-watched-description',
-                                               'cssClassSuffix' => 'watched',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return $rc->getAttribute( 'wl_user' );
-                                               }
-                                       ],
-                                       [
-                                               'name' => 'watchednew',
-                                               'label' => 'rcfilters-filter-watchlist-watchednew-label',
-                                               'description' => 'rcfilters-filter-watchlist-watchednew-description',
-                                               'cssClassSuffix' => 'watchednew',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return $rc->getAttribute( 'wl_user' ) &&
-                                                               $rc->getAttribute( 'rc_timestamp' ) &&
-                                                               $rc->getAttribute( 'wl_notificationtimestamp' ) &&
-                                                               $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'wl_notificationtimestamp' );
-                                               },
-                                       ],
-                                       [
-                                               'name' => 'notwatched',
-                                               'label' => 'rcfilters-filter-watchlist-notwatched-label',
-                                               'description' => 'rcfilters-filter-watchlist-notwatched-description',
-                                               'cssClassSuffix' => 'notwatched',
-                                               'isRowApplicableCallable' => function ( $ctx, $rc ) {
-                                                       return $rc->getAttribute( 'wl_user' ) === null;
-                                               },
-                                       ]
-                               ],
-                               'default' => ChangesListStringOptionsFilterGroup::NONE,
-                               'queryCallable' => function ( $specialPageClassName, $context, $dbr,
-                                       &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
-                                       sort( $selectedValues );
-                                       $notwatchedCond = 'wl_user IS NULL';
-                                       $watchedCond = 'wl_user IS NOT NULL';
-                                       $newCond = 'rc_timestamp >= wl_notificationtimestamp';
-
-                                       if ( $selectedValues === [ 'notwatched' ] ) {
-                                               $conds[] = $notwatchedCond;
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'watched' ] ) {
-                                               $conds[] = $watchedCond;
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'watchednew' ] ) {
-                                               $conds[] = $dbr->makeList( [
-                                                       $watchedCond,
-                                                       $newCond
-                                               ], LIST_AND );
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
-                                               // no filters
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
-                                               $conds[] = $dbr->makeList( [
-                                                       $notwatchedCond,
-                                                       $dbr->makeList( [
-                                                               $watchedCond,
-                                                               $newCond
-                                                       ], LIST_AND )
-                                               ], LIST_OR );
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
-                                               $conds[] = $watchedCond;
-                                               return;
-                                       }
-
-                                       if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
-                                               // no filters
-                                               return;
-                                       }
-                               },
-                       ],
                ];
 
                $this->reviewStatusFilterGroupDefinition = [
@@ -700,7 +611,7 @@ abstract class ChangesListSpecialPage extends SpecialPage {
 
                // Make sure this is not being transcluded (we don't want to show this
                // information to all users just because the user that saves the edit can
-               // patrol)
+               // patrol or is logged in)
                if ( !$this->including() && $this->getUser()->useRCPatrol() ) {
                        $this->registerFiltersFromDefinitions( $this->reviewStatusFilterGroupDefinition );
                }
@@ -770,11 +681,6 @@ abstract class ChangesListSpecialPage extends SpecialPage {
                        'rcfilters-hideminor-conflicts-typeofchange',
                        'rcfilters-typeofchange-conflicts-hideminor'
                );
-
-               $watchlistGroup = $this->getFilterGroup( 'watchlist' );
-               $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
-                       $watchlistGroup->getFilter( 'watchednew' )
-               );
        }
 
        /**
index aaa99b6..a47d91b 100644 (file)
@@ -31,9 +31,104 @@ use Wikimedia\Rdbms\FakeResultWrapper;
  * @ingroup SpecialPage
  */
 class SpecialRecentChanges extends ChangesListSpecialPage {
+
+       private $watchlistFilterGroupDefinition;
+
        // @codingStandardsIgnoreStart Needed "useless" override to change parameters.
        public function __construct( $name = 'Recentchanges', $restriction = '' ) {
                parent::__construct( $name, $restriction );
+
+               $this->watchlistFilterGroupDefinition = [
+                       'name' => 'watchlist',
+                       'title' => 'rcfilters-filtergroup-watchlist',
+                       'class' => ChangesListStringOptionsFilterGroup::class,
+                       'priority' => -9,
+                       'isFullCoverage' => true,
+                       'filters' => [
+                               [
+                                       'name' => 'watched',
+                                       'label' => 'rcfilters-filter-watchlist-watched-label',
+                                       'description' => 'rcfilters-filter-watchlist-watched-description',
+                                       'cssClassSuffix' => 'watched',
+                                       'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                               return $rc->getAttribute( 'wl_user' );
+                                       }
+                               ],
+                               [
+                                       'name' => 'watchednew',
+                                       'label' => 'rcfilters-filter-watchlist-watchednew-label',
+                                       'description' => 'rcfilters-filter-watchlist-watchednew-description',
+                                       'cssClassSuffix' => 'watchednew',
+                                       'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                               return $rc->getAttribute( 'wl_user' ) &&
+                                                       $rc->getAttribute( 'rc_timestamp' ) &&
+                                                       $rc->getAttribute( 'wl_notificationtimestamp' ) &&
+                                                       $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'wl_notificationtimestamp' );
+                                       },
+                               ],
+                               [
+                                       'name' => 'notwatched',
+                                       'label' => 'rcfilters-filter-watchlist-notwatched-label',
+                                       'description' => 'rcfilters-filter-watchlist-notwatched-description',
+                                       'cssClassSuffix' => 'notwatched',
+                                       'isRowApplicableCallable' => function ( $ctx, $rc ) {
+                                               return $rc->getAttribute( 'wl_user' ) === null;
+                                       },
+                               ]
+                       ],
+                       'default' => ChangesListStringOptionsFilterGroup::NONE,
+                       'queryCallable' => function ( $specialPageClassName, $context, $dbr,
+                               &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) {
+                               sort( $selectedValues );
+                               $notwatchedCond = 'wl_user IS NULL';
+                               $watchedCond = 'wl_user IS NOT NULL';
+                               $newCond = 'rc_timestamp >= wl_notificationtimestamp';
+
+                               if ( $selectedValues === [ 'notwatched' ] ) {
+                                       $conds[] = $notwatchedCond;
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'watched' ] ) {
+                                       $conds[] = $watchedCond;
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'watchednew' ] ) {
+                                       $conds[] = $dbr->makeList( [
+                                               $watchedCond,
+                                               $newCond
+                                       ], LIST_AND );
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'notwatched', 'watched' ] ) {
+                                       // no filters
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'notwatched', 'watchednew' ] ) {
+                                       $conds[] = $dbr->makeList( [
+                                               $notwatchedCond,
+                                               $dbr->makeList( [
+                                                       $watchedCond,
+                                                       $newCond
+                                               ], LIST_AND )
+                                       ], LIST_OR );
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'watched', 'watchednew' ] ) {
+                                       $conds[] = $watchedCond;
+                                       return;
+                               }
+
+                               if ( $selectedValues === [ 'notwatched', 'watched', 'watchednew' ] ) {
+                                       // no filters
+                                       return;
+                               }
+                       }
+               ];
        }
        // @codingStandardsIgnoreEnd
 
@@ -103,6 +198,18 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
        protected function registerFilters() {
                parent::registerFilters();
 
+               if (
+                       !$this->including() &&
+                       $this->getUser()->isLoggedIn() &&
+                       $this->getUser()->isAllowed( 'viewmywatchlist' )
+               ) {
+                       $this->registerFiltersFromDefinitions( [ $this->watchlistFilterGroupDefinition ] );
+                       $watchlistGroup = $this->getFilterGroup( 'watchlist' );
+                       $watchlistGroup->getFilter( 'watched' )->setAsSupersetOf(
+                               $watchlistGroup->getFilter( 'watchednew' )
+                       );
+               }
+
                $user = $this->getUser();
 
                $significance = $this->getFilterGroup( 'significance' );
@@ -236,7 +343,7 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                $fields = array_merge( RecentChange::selectFields(), $fields );
 
                // JOIN on watchlist for users
-               if ( $user->getId() && $user->isAllowed( 'viewmywatchlist' ) ) {
+               if ( $user->isLoggedIn() && $user->isAllowed( 'viewmywatchlist' ) ) {
                        $tables[] = 'watchlist';
                        $fields[] = 'wl_user';
                        $fields[] = 'wl_notificationtimestamp';
@@ -247,11 +354,10 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        ] ];
                }
 
-               if ( $user->isAllowed( 'rollback' ) ) {
-                       $tables[] = 'page';
-                       $fields[] = 'page_latest';
-                       $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
-               }
+               // JOIN on page, used for 'last revision' filter highlight
+               $tables[] = 'page';
+               $fields[] = 'page_latest';
+               $join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
 
                ChangeTags::modifyDisplayQuery(
                        $tables,
index 46c5a00..feaad12 100644 (file)
  * DEALINGS IN THE SOFTWARE.
  */
 
-require_once __DIR__ . '/Maintenance.php';
-
-/**
- * Maintenance script that builds doxygen documentation.
- * @ingroup Maintenance
- */
-class MWDocGenFilter extends Maintenance {
-       public function __construct() {
-               parent::__construct();
-               $this->addDescription( 'Doxygen filter to fix member variable types in documentation. '
-                       . 'Used by mwdocgen.php'
-               );
-               $this->addArg( 'filename', 'PHP file to filter', true );
-       }
+// Warning: Converting this to a Maintenance script may reduce performance.
+if ( PHP_SAPI != 'cli' ) {
+       die( "This filter can only be run from the command line.\n" );
+}
 
-       public function execute() {
-               $source = file_get_contents( $this->getArg( 0 ) );
-               $tokens = token_get_all( $source );
+$source = file_get_contents( $argv[1] );
+$tokens = token_get_all( $source );
 
-               $buffer = $bufferType = null;
-               foreach ( $tokens as $token ) {
-                       if ( is_string( $token ) ) {
-                               if ( $buffer !== null && $token === ';' ) {
-                                       // If we still have a buffer and the statement has ended,
-                                       // flush it and move on.
-                                       echo $buffer;
-                                       $buffer = $bufferType = null;
-                               }
-                               echo $token;
-                               continue;
+$buffer = $bufferType = null;
+foreach ( $tokens as $token ) {
+       if ( is_string( $token ) ) {
+               if ( $buffer !== null && $token === ';' ) {
+                       // If we still have a buffer and the statement has ended,
+                       // flush it and move on.
+                       echo $buffer;
+                       $buffer = $bufferType = null;
+               }
+               echo $token;
+               continue;
+       }
+       list( $id, $content ) = $token;
+       switch ( $id ) {
+               case T_DOC_COMMENT:
+                       // Escape slashes so that references to namespaces are not
+                       // wrongly interpreted as a Doxygen "\command".
+                       $content = addcslashes( $content, '\\' );
+                       // Look for instances of "@var Type" not followed by $name.
+                       if ( preg_match( '#@var\s+([^\s]+)\s+([^\$]+)#s', $content ) ) {
+                               $buffer = preg_replace_callback(
+                                       // Strip the "@var Type" part and remember the type
+                                       '#(@var\s+)([^\s]+)#s',
+                                       function ( $matches ) use ( &$bufferType ) {
+                                               $bufferType = $matches[2];
+                                               return '';
+                                       },
+                                       $content
+                               );
+                       } else {
+                               echo $content;
                        }
-                       list( $id, $content ) = $token;
-                       switch ( $id ) {
-                               case T_DOC_COMMENT:
-                                       // Escape slashes so that references to namespaces are not
-                                       // wrongly interpreted as a Doxygen "\command".
-                                       $content = addcslashes( $content, '\\' );
-                                       // Look for instances of "@var Type" not followed by $name.
-                                       if ( preg_match( '#@var\s+([^\s]+)\s+([^\$]+)#s', $content ) ) {
-                                               $buffer = preg_replace_callback(
-                                                       // Strip the "@var Type" part and remember the type
-                                                       '#(@var\s+)([^\s]+)#s',
-                                                       function ( $matches ) use ( &$bufferType ) {
-                                                               $bufferType = $matches[2];
-                                                               return '';
-                                                       },
-                                                       $content
-                                               );
-                                       } else {
-                                               echo $content;
-                                       }
-                                       break;
+                       break;
 
-                               case T_VARIABLE:
-                                       if ( $buffer !== null ) {
-                                               echo $buffer;
-                                               echo "$bufferType $content";
-                                               $buffer = $bufferType = null;
-                                       } else {
-                                               echo $content;
-                                       }
-                                       break;
+               case T_VARIABLE:
+                       if ( $buffer !== null ) {
+                               echo $buffer;
+                               echo "$bufferType $content";
+                               $buffer = $bufferType = null;
+                       } else {
+                               echo $content;
+                       }
+                       break;
 
-                               default:
-                                       if ( $buffer !== null ) {
-                                               $buffer .= $content;
-                                       } else {
-                                               echo $content;
-                                       }
-                                       break;
+               default:
+                       if ( $buffer !== null ) {
+                               $buffer .= $content;
+                       } else {
+                               echo $content;
                        }
-               }
+                       break;
        }
 }
-
-$maintClass = 'MWDocGenFilter';
-require_once RUN_MAINTENANCE_IF_MAIN;
index dc2eccd..d005629 100644 (file)
@@ -72,7 +72,7 @@ class MWDocGen extends Maintenance {
        }
 
        protected function init() {
-               global $IP;
+               global $wgPhpCli, $IP;
 
                $this->doxygen = $this->getOption( 'doxygen', 'doxygen' );
                $this->mwVersion = $this->getOption( 'version', 'master' );
@@ -86,7 +86,13 @@ class MWDocGen extends Maintenance {
 
                $this->output = $this->getOption( 'output', "$IP/docs" );
 
-               $this->inputFilter = wfShellWikiCmd( $IP . '/maintenance/mwdoc-filter.php' );
+               // Do not use wfShellWikiCmd, because mwdoc-filter.php is not
+               // a Maintenance script.
+               $this->inputFilter = wfEscapeShellArg( [
+                       $wgPhpCli,
+                       $IP . '/maintenance/mwdoc-filter.php'
+               ] );
+
                $this->template = $IP . '/maintenance/Doxyfile';
                $this->excludes = [
                        'vendor',
diff --git a/tests/phpunit/data/media/adobergb.jpg b/tests/phpunit/data/media/adobergb.jpg
new file mode 100644 (file)
index 0000000..470c2d6
Binary files /dev/null and b/tests/phpunit/data/media/adobergb.jpg differ
diff --git a/tests/phpunit/data/media/missingprofile.jpg b/tests/phpunit/data/media/missingprofile.jpg
new file mode 100644 (file)
index 0000000..4085f0a
Binary files /dev/null and b/tests/phpunit/data/media/missingprofile.jpg differ
index b965dc4..f10ced0 100644 (file)
Binary files a/tests/phpunit/data/media/srgb.jpg and b/tests/phpunit/data/media/srgb.jpg differ
index 12a8e09..63b687e 100644 (file)
Binary files a/tests/phpunit/data/media/tinyrgb.jpg and b/tests/phpunit/data/media/tinyrgb.jpg differ
index 47ed67b..3dd7e4c 100644 (file)
@@ -142,61 +142,4 @@ class ExifBitmapTest extends MediaWikiMediaTestCase {
                $res = $this->handler->convertMetadataVersion( $metadata, 1 );
                $this->assertEquals( $expected, $res );
        }
-
-       /**
-        * @dataProvider provideSwappingICCProfile
-        * @covers ExifBitmapHandler::swapICCProfile
-        */
-       public function testSwappingICCProfile(
-               $sourceFilename, $controlFilename, $newProfileFilename, $oldProfileName
-       ) {
-               global $wgExiftool;
-
-               if ( !$wgExiftool || !is_file( $wgExiftool ) ) {
-                       $this->markTestSkipped( "Exiftool not installed, cannot test ICC profile swapping" );
-               }
-
-               $this->setMwGlobals( 'wgUseTinyRGBForJPGThumbnails', true );
-
-               $sourceFilepath = $this->filePath . $sourceFilename;
-               $controlFilepath = $this->filePath . $controlFilename;
-               $profileFilepath = $this->filePath . $newProfileFilename;
-               $filepath = $this->getNewTempFile();
-
-               copy( $sourceFilepath, $filepath );
-
-               $file = $this->dataFile( $sourceFilename, 'image/jpeg' );
-               $this->handler->swapICCProfile( $filepath, $oldProfileName, $profileFilepath );
-
-               $this->assertEquals(
-                       sha1( file_get_contents( $filepath ) ),
-                       sha1( file_get_contents( $controlFilepath ) )
-               );
-       }
-
-       public function provideSwappingICCProfile() {
-               return [
-                       // File with sRGB should end up with TinyRGB
-                       [
-                               'srgb.jpg',
-                               'tinyrgb.jpg',
-                               'tinyrgb.icc',
-                               'IEC 61966-2.1 Default RGB colour space - sRGB'
-                       ],
-                       // File with TinyRGB should be left unchanged
-                       [
-                               'tinyrgb.jpg',
-                               'tinyrgb.jpg',
-                               'tinyrgb.icc',
-                               'IEC 61966-2.1 Default RGB colour space - sRGB'
-                       ],
-                       // File with no profile should be left unchanged
-                       [
-                               'test.jpg',
-                               'test.jpg',
-                               'tinyrgb.icc',
-                               'IEC 61966-2.1 Default RGB colour space - sRGB'
-                       ]
-               ];
-       }
 }
index 05aed4a..b0f40ef 100644 (file)
@@ -51,4 +51,73 @@ class JpegTest extends MediaWikiMediaTestCase {
 
                $this->assertEquals( $res, $expected );
        }
+
+       /**
+        * @dataProvider provideSwappingICCProfile
+        * @covers ExifBitmapHandler::swapICCProfile
+        */
+       public function testSwappingICCProfile(
+               $sourceFilename, $controlFilename, $newProfileFilename, $oldProfileName
+       ) {
+               global $wgExiftool;
+
+               if ( !$wgExiftool || !is_file( $wgExiftool ) ) {
+                       $this->markTestSkipped( "Exiftool not installed, cannot test ICC profile swapping" );
+               }
+
+               $this->setMwGlobals( 'wgUseTinyRGBForJPGThumbnails', true );
+
+               $sourceFilepath = $this->filePath . $sourceFilename;
+               $controlFilepath = $this->filePath . $controlFilename;
+               $profileFilepath = $this->filePath . $newProfileFilename;
+               $filepath = $this->getNewTempFile();
+
+               copy( $sourceFilepath, $filepath );
+
+               $file = $this->dataFile( $sourceFilename, 'image/jpeg' );
+               $this->handler->swapICCProfile(
+                       $filepath,
+                       [ 'sRGB', '-' ],
+                       [ $oldProfileName ],
+                       $profileFilepath
+               );
+
+               $this->assertEquals(
+                       sha1( file_get_contents( $filepath ) ),
+                       sha1( file_get_contents( $controlFilepath ) )
+               );
+       }
+
+       public function provideSwappingICCProfile() {
+               return [
+                       // File with sRGB should end up with TinyRGB
+                       [
+                               'srgb.jpg',
+                               'tinyrgb.jpg',
+                               'tinyrgb.icc',
+                               'sRGB IEC61966-2.1'
+                       ],
+                       // File with TinyRGB should be left unchanged
+                       [
+                               'tinyrgb.jpg',
+                               'tinyrgb.jpg',
+                               'tinyrgb.icc',
+                               'sRGB IEC61966-2.1'
+                       ],
+                       // File without profile should end up with TinyRGB
+                       [
+                               'missingprofile.jpg',
+                               'tinyrgb.jpg',
+                               'tinyrgb.icc',
+                               'sRGB IEC61966-2.1'
+                       ],
+                       // Non-sRGB file should be left untouched
+                       [
+                               'adobergb.jpg',
+                               'adobergb.jpg',
+                               'tinyrgb.icc',
+                               'sRGB IEC61966-2.1'
+                       ]
+               ];
+       }
 }