* The Block class constructor now takes an associative array of parameters
instead of many optional positional arguments. Calling the constructor the old
way will issue a deprecation warning.
+* The jquery.mwExtension module was deprecated.
== Compatibility ==
*/
$wgExiv2Command = '/usr/bin/exiv2';
+
+/**
+ * Path to exiftool binary. Used for lossless ICC profile swapping.
+ *
+ * @since 1.26
+ */
+$wgExiftool = '/usr/bin/exiftool';
+
/**
* Scalable Vector Graphics (SVG) may be uploaded as images.
* Since SVG support is not yet standard in browsers, it is
*/
$wgUploadThumbnailRenderHttpCustomDomain = false;
+/**
+ * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight
+ * (and free) TinyRGB profile when generating thumbnails.
+ *
+ * @since 1.26
+ */
+$wgUseTinyRGBForJPGThumbnails = false;
+
/**
* Default parameters for the "<gallery>" tag
*/
protected function transformImageMagick( $image, $params ) {
# use ImageMagick
global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
- $wgImageMagickTempDir, $wgImageMagickConvertCommand;
+ $wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgResourceBasePath,
+ $wgUseTinyRGBForJPGThumbnails;
$quality = array();
$sharpen = array();
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.
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;
+ }
}
$rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
- if ( $wgJpegTran && is_file( $wgJpegTran ) ) {
+ if ( $wgJpegTran && is_executable( $wgJpegTran ) ) {
$cmd = wfEscapeShellArg( $wgJpegTran ) .
" -rotate " . wfEscapeShellArg( $rotation ) .
" -outfile " . wfEscapeShellArg( $params['dstPath'] ) .
"nmembers": "$1 {{PLURAL:$1|member|members}}",
"nmemberschanged": "$1 → $2 {{PLURAL:$2|member|members}}",
"nrevisions": "$1 {{PLURAL:$1|revision|revisions}}",
- "nviews": "$1 {{PLURAL:$1|view|views}}",
"nimagelinks": "Used on $1 {{PLURAL:$1|page|pages}}",
"ntransclusions": "used on $1 {{PLURAL:$1|page|pages}}",
"specialpage-empty": "There are no results for this report.",
"nmembers": "Appears in brackets after each category listed on the special page [[Special:WantedCategories]].\n\nParameters:\n* $1 - the number of members of the category\nSee also:\n* {{msg-mw|Nmemberschanged}}",
"nmemberschanged": "Appears in brackets after each category listed on the special page [[Special:WantedCategories]] if the number of pages in the category has changed since the list was last refreshed.\n\nParameters:\n* $1 - the original number of members of the category\n* $2 - the current one\nSee also:\n* {{msg-mw|Nmembers}}",
"nrevisions": "Used as link text in [[Special:FewestRevisions]].\n\nThe link points to the page history (action=history).\n\nParameters:\n* $1 - number of revisions",
- "nviews": "This message is used on [[Special:PopularPages]] to say how many times each page has been viewed.\n\nPreceded by the page title, like: Page title ($1 views)\n\nParameters:\n* $1 - the number of views",
"nimagelinks": "Used on [[Special:MostLinkedFiles]] to indicate how often a specific file is used.\n\nParameters:\n* $1 - number of pages\nSee also:\n* {{msg-mw|Ntransclusions}}",
"ntransclusions": "Used on [[Special:MostTranscludedPages]] to indicate how often a template is in use.\n\nParameters:\n* $1 - number of pages\nSee also:\n* {{msg-mw|Nimagelinks}}",
"specialpage-empty": "Used on a special page when there is no data. For example on [[Special:Unusedimages]] when all images are used.",
'PageLanguage' => array( 'PageLanguage' ),
'PasswordReset' => array( 'PasswordReset' ),
'PermanentLink' => array( 'PermanentLink', 'PermaLink' ),
- 'Popularpages' => array( 'PopularPages' ),
'Preferences' => array( 'Preferences' ),
'Prefixindex' => array( 'PrefixIndex' ),
'Protectedpages' => array( 'ProtectedPages' ),
excludeuser
executables
exempt
+exiftool
existingwiki
exists
exiv
pointsize
poolcounter
popts
-popularpages
portlet
portlets
posplus
"classes": [
"mw.Title",
"mw.Uri",
+ "mw.RegExp",
"mw.messagePoster.*",
"mw.notification",
"mw.Notification_",
'scripts' => 'resources/src/jquery/jquery.accessKeyLabel.js',
'dependencies' => array(
'jquery.client',
- 'jquery.mwExtension',
+ 'mediawiki.RegExp',
),
'messages' => array( 'brackets', 'word-separator' ),
'targets' => array( 'mobile', 'desktop' ),
),
'jquery.highlightText' => array(
'scripts' => 'resources/src/jquery/jquery.highlightText.js',
- 'dependencies' => 'jquery.mwExtension',
+ 'dependencies' => 'mediawiki.RegExp',
'targets' => array( 'desktop', 'mobile' ),
),
'jquery.hoverIntent' => array(
'styles' => 'resources/src/jquery/jquery.tablesorter.css',
'messages' => array( 'sort-descending', 'sort-ascending' ),
'dependencies' => array(
- 'jquery.mwExtension',
+ 'mediawiki.RegExp',
'mediawiki.language.months',
),
),
'mediawiki.htmlform' => array(
'scripts' => 'resources/src/mediawiki/mediawiki.htmlform.js',
'dependencies' => array(
- 'jquery.mwExtension',
+ 'mediawiki.RegExp',
'jquery.byteLimit',
),
'messages' => array(
'scripts' => 'resources/src/mediawiki/mediawiki.inspect.js',
'dependencies' => array(
'jquery.byteLength',
+ 'mediawiki.RegExp',
'json',
),
'targets' => array( 'desktop', 'mobile' ),
'scripts' => 'resources/src/mediawiki/mediawiki.notify.js',
'targets' => array( 'desktop', 'mobile' ),
),
+ 'mediawiki.RegExp' => array(
+ 'scripts' => 'resources/src/mediawiki/mediawiki.RegExp.js',
+ 'targets' => array( 'desktop', 'mobile' ),
+ ),
'mediawiki.pager.tablePager' => array(
'styles' => 'resources/src/mediawiki/mediawiki.pager.tablePager.less',
'position' => 'top',
'scripts' => 'resources/src/mediawiki/mediawiki.util.js',
'dependencies' => array(
'jquery.accessKeyLabel',
- 'jquery.mwExtension',
+ 'mediawiki.RegExp',
'mediawiki.notify',
),
'position' => 'top', // For $wgPreloadJavaScriptMwUtil
'mediawiki.page.startup',
'mediawiki.util',
'jquery.accessKeyLabel',
- 'jquery.mwExtension',
+ 'mediawiki.RegExp',
),
'messages' => array(
'watch',
*/
function updateTooltipOnElement( element, titleElement ) {
var array = ( mw.msg( 'word-separator' ) + mw.msg( 'brackets' ) ).split( '$1' ),
- regexp = new RegExp( $.map( array, $.escapeRE ).join( '.*?' ) + '$' ),
+ regexp = new RegExp( $.map( array, mw.RegExp.escape ).join( '.*?' ) + '$' ),
oldTitle = titleElement.title,
rawTitle = oldTitle.replace( regexp, '' ),
newTitle = rawTitle,
* TODO: Add a function for restoring the previous text.
* TODO: Accept mappings for converting shortcuts like WP: to Wikipedia:.
*/
-( function ( $ ) {
+( function ( $, mw ) {
$.highlightText = {
// non latin characters can make regex think a new word has begun: do not use \b
// http://stackoverflow.com/questions/3787072/regex-wordwrap-with-utf8-characters-in-js
// look for an occurrence of our pattern and store the starting position
- match = node.data.match( new RegExp( '(^|\\s)' + $.escapeRE( pat ), 'i' ) );
+ match = node.data.match( new RegExp( '(^|\\s)' + mw.RegExp.escape( pat ), 'i' ) );
if ( match ) {
pos = match.index + match[1].length; // include length of any matched spaces
// create the span wrapper for the matched text
} );
};
-}( jQuery ) );
+}( jQuery, mediaWiki ) );
/*
* JavaScript backwards-compatibility alternatives and other convenience functions
+ *
+ * @deprecated since 1.26 Dated collection of miscellaneous utilities. Methods are
+ * either trivially inline, obsolete, or have a better place elsewhere.
*/
-( function ( $ ) {
-
- $.extend( {
+( function ( $, mw ) {
+ $.each( {
trimLeft: function ( str ) {
return str === null ? '' : str.toString().replace( /^\s+/, '' );
},
ucFirst: function ( str ) {
return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
},
- escapeRE: function ( str ) {
- return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
- },
isDomElement: function ( el ) {
return !!el && !!el.nodeType;
},
return true;
}
// the for-loop could potentially contain prototypes
- // to avoid that we check it's length first
+ // to avoid that we check its length first
if ( v.length === 0 ) {
return true;
}
}
return true;
}
+ }, function ( key, value ) {
+ mw.log.deprecate( $, key, value );
} );
-}( jQuery ) );
+ mw.log.deprecate( $, 'escapeRE', function ( str ) {
+ return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ }, 'Use mediawiki.RegExp instead.' );
+
+} )( jQuery, mediaWiki );
// Construct regex for number identification
for ( i = 0; i < ascii.length; i++ ) {
ts.transformTable[localised[i]] = ascii[i];
- digits.push( $.escapeRE( localised[i] ) );
+ digits.push( mw.RegExp.escape( localised[i] ) );
}
}
digitClass = '[' + digits.join( '', digits ) + ']';
for ( i = 0; i < 12; i++ ) {
name = mw.language.months.names[i].toLowerCase();
ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
+ regex.push( mw.RegExp.escape( name ) );
name = mw.language.months.genitive[i].toLowerCase();
ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
+ regex.push( mw.RegExp.escape( name ) );
name = mw.language.months.abbrev[i].toLowerCase().replace( '.', '' );
ts.monthNames[name] = i + 1;
- regex.push( $.escapeRE( name ) );
+ regex.push( mw.RegExp.escape( name ) );
}
// Build piped string
if ( actionPaths.hasOwnProperty( key ) ) {
parts = actionPaths[key].split( '$1' );
for ( i = 0; i < parts.length; i++ ) {
- parts[i] = $.escapeRE( parts[i] );
+ parts[i] = mw.RegExp.escape( parts[i] );
}
m = new RegExp( parts.join( '(.+)' ) ).exec( url );
if ( m && m[1] ) {
--- /dev/null
+( function ( mw ) {
+ /**
+ * @class mw.RegExp
+ */
+ mw.RegExp = {
+ /**
+ * Escape string for safe inclusion in regular expression
+ *
+ * The following characters are escaped:
+ *
+ * \ { } ( ) | . ? * + - ^ $ [ ]
+ *
+ * @since 1.26
+ * @static
+ * @param {string} str String to escape
+ * @return {string} Escaped string
+ */
+ escape: function ( str ) {
+ return str.replace( /([\\{}()|.?*+\-\^$\[\]])/g, '\\$1' );
+ }
+ };
+}( mediaWiki ) );
$ul = $( this ).prev( 'ul.mw-htmlform-cloner-ul' );
html = $ul.data( 'template' ).replace(
- new RegExp( $.escapeRE( $ul.data( 'uniqueId' ) ), 'g' ),
+ new RegExp( mw.RegExp.escape( $ul.data( 'uniqueId' ) ), 'g' ),
'clone' + ( ++cloneCounter )
);
*/
grep: function ( pattern ) {
if ( typeof pattern.test !== 'function' ) {
- // Based on Y.Escape.regex from YUI v3.15.0
- pattern = new RegExp( pattern.replace( /[\-$\^*()+\[\]{}|\\,.?\s]/g, '\\$&' ), 'g' );
+ pattern = new RegExp( mw.RegExp.escape( pattern ), 'g' );
}
return $.grep( inspect.getLoadedModules(), function ( moduleName ) {
url = location.href;
}
// Get last match, stop at hash
- var re = new RegExp( '^[^#]*[&?]' + $.escapeRE( param ) + '=([^&#]*)' ),
+ var re = new RegExp( '^[^#]*[&?]' + mw.RegExp.escape( param ) + '=([^&#]*)' ),
m = re.exec( url );
if ( m ) {
// Beware that decodeURIComponent is not required to understand '+'
</td><td>
<template lineStart="1"><title>int:Nstab-wp</title></template>
</td></tr><tr><td>
-[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nviews&action=edit nviews]<br>
-[[MediaWiki_talk:Nviews|Talk]]
-</td><td>
-$1 views
-</td><td>
-<template lineStart="1"><title>int:Nviews</title></template>
-</td></tr><tr><td>
[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ok&action=edit ok]<br>
[[MediaWiki_talk:Ok|Talk]]
</td><td>
</td><td>
<template lineStart="1"><title>int:Personaltools</title></template>
</td></tr><tr><td>
-[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Popularpages&action=edit popularpages]<br>
-[[MediaWiki_talk:Popularpages|Talk]]
-</td><td>
-Popular pages
-</td><td>
-<template lineStart="1"><title>int:Popularpages</title></template>
-</td></tr><tr><td>
[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal&action=edit portal]<br>
[[MediaWiki_talk:Portal|Talk]]
</td><td>
</td><td>
{{int:Nstab-wp}}
</td></tr><tr><td>
-[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Nviews&action=edit nviews]<br>
-[[MediaWiki_talk:Nviews|Talk]]
-</td><td>
-$1 views
-</td><td>
-{{int:Nviews}}
-</td></tr><tr><td>
[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Ok&action=edit ok]<br>
[[MediaWiki_talk:Ok|Talk]]
</td><td>
</td><td>
{{int:Personaltools}}
</td></tr><tr><td>
-[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Popularpages&action=edit popularpages]<br>
-[[MediaWiki_talk:Popularpages|Talk]]
-</td><td>
-Popular pages
-</td><td>
-{{int:Popularpages}}
-</td></tr><tr><td>
[http://tl.wiktionary.org/w/wiki.phtml?title=MediaWiki:Portal&action=edit portal]<br>
[[MediaWiki_talk:Portal|Talk]]
</td><td>
/**
* @group Media
*/
-class ExifBitmapTest extends MediaWikiTestCase {
+class ExifBitmapTest extends MediaWikiMediaTestCase {
/**
* @var ExifBitmapHandler
$res = $this->handler->convertMetadataVersion( $metadata, 1 );
$this->assertEquals( $expected, $res );
}
+
+ /**
+ * @dataProvider provideSwappingICCProfile
+ * @covers BitmapHandler::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 array(
+ // File with sRGB should end up with TinyRGB
+ array( 'srgb.jpg', 'tinyrgb.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' ),
+ // File with TinyRGB should be left unchanged
+ array( 'tinyrgb.jpg', 'tinyrgb.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' ),
+ // File with no profile should be left unchanged
+ array( 'test.jpg', 'test.jpg', 'tinyrgb.icc', 'IEC 61966-2.1 Default RGB colour space - sRGB' )
+ );
+ }
}
'tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.jscompat.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.messagePoster.factory.test.js',
+ 'tests/qunit/suites/resources/mediawiki/mediawiki.RegExp.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.template.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.test.js',
'tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js',
'mediawiki.api.watch',
'mediawiki.jqueryMsg',
'mediawiki.messagePoster',
+ 'mediawiki.RegExp',
'mediawiki.Title',
'mediawiki.toc',
'mediawiki.Uri',
--- /dev/null
+( function ( mw, $ ) {
+ QUnit.module( 'mediawiki.RegExp' );
+
+ QUnit.test( 'escape', 16, function ( assert ) {
+ var specials, normal;
+
+ specials = [
+ '\\',
+ '{',
+ '}',
+ '(',
+ ')',
+ '[',
+ ']',
+ '|',
+ '.',
+ '?',
+ '*',
+ '+',
+ '-',
+ '^',
+ '$'
+ ];
+
+ normal = [
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ 'abcdefghijklmnopqrstuvwxyz',
+ '0123456789'
+ ].join( '' );
+
+ $.each( specials, function ( i, str ) {
+ assert.propEqual( str.match( new RegExp( mw.RegExp.escape( str ) ) ), [ str ], 'Match ' + str );
+ } );
+
+ assert.equal( mw.RegExp.escape( normal ), normal, 'Alphanumerals are left alone' );
+ } );
+
+}( mediaWiki, jQuery ) );