* Revision::setUserIdAndName() was deprecated.
* Access to TitleValue class properties was deprecated, the relevant getters
should be used instead.
+* DifferenceEngine::getDiffBodyCacheKey() is deprecated. Subclasses should
+ override DifferenceEngine::getDiffBodyCacheKeyParams() instead.
+* The deprecated MW_DIFF_VERSION constant was removed.
+ DifferenceEngine::MW_DIFF_VERSION should be used instead.
== Compatibility ==
MediaWiki 1.31 requires PHP 5.5.9 or later. There is experimental support for
*/
const PARAM_ISMULTI_LIMIT2 = 22;
+ /**
+ * (integer) Maximum length of a string in bytes (in UTF-8 encoding).
+ * @since 1.31
+ */
+ const PARAM_MAX_BYTES = 23;
+
+ /**
+ * (integer) Maximum length of a string in characters (unicode codepoints).
+ * @since 1.31
+ */
+ const PARAM_MAX_CHARS = 24;
+
/**@}*/
const ALL_DEFAULT_STRING = '*';
);
}
- // More validation only when choices were not given
- // choices were validated in parseMultiValue()
if ( isset( $value ) ) {
+ // More validation only when choices were not given
+ // choices were validated in parseMultiValue()
if ( !is_array( $type ) ) {
switch ( $type ) {
case 'NULL': // nothing to do
$value = array_unique( $value );
}
+ if ( in_array( $type, [ 'NULL', 'string', 'text', 'password' ], true ) ) {
+ foreach ( (array)$value as $val ) {
+ if ( isset( $paramSettings[self::PARAM_MAX_BYTES] )
+ && strlen( $val ) > $paramSettings[self::PARAM_MAX_BYTES]
+ ) {
+ $this->dieWithError( [ 'apierror-maxbytes', $encParamName,
+ $paramSettings[self::PARAM_MAX_BYTES] ] );
+ }
+ if ( isset( $paramSettings[self::PARAM_MAX_CHARS] )
+ && mb_strlen( $val, 'UTF-8' ) > $paramSettings[self::PARAM_MAX_CHARS]
+ ) {
+ $this->dieWithError( [ 'apierror-maxchars', $encParamName,
+ $paramSettings[self::PARAM_MAX_CHARS] ] );
+ }
+ }
+ }
+
// Set a warning if a deprecated parameter has been passed
if ( $deprecated && $value !== false ) {
$feature = $encParamName;
}
}
+ if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
+ $info[] = $context->msg( 'api-help-param-maxbytes' )
+ ->numParams( $settings[self::PARAM_MAX_BYTES] );
+ }
+ if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
+ $info[] = $context->msg( 'api-help-param-maxchars' )
+ ->numParams( $settings[self::PARAM_MAX_CHARS] );
+ }
+
// Add default
$default = isset( $settings[ApiBase::PARAM_DFLT] )
? $settings[ApiBase::PARAM_DFLT]
if ( !empty( $settings[ApiBase::PARAM_RANGE_ENFORCE] ) ) {
$item['enforcerange'] = true;
}
+ if ( isset( $settings[self::PARAM_MAX_BYTES] ) ) {
+ $item['maxbytes'] = $settings[self::PARAM_MAX_BYTES];
+ }
+ if ( isset( $settings[self::PARAM_MAX_CHARS] ) ) {
+ $item['maxchars'] = $settings[self::PARAM_MAX_CHARS];
+ }
if ( !empty( $settings[ApiBase::PARAM_DEPRECATED_VALUES] ) ) {
$deprecatedValues = array_keys( $settings[ApiBase::PARAM_DEPRECATED_VALUES] );
if ( is_array( $item['type'] ) ) {
"api-help-param-direction": "In which direction to enumerate:\n;newer:List oldest first. Note: $1start has to be before $1end.\n;older:List newest first (default). Note: $1start has to be later than $1end.",
"api-help-param-continue": "When more results are available, use this to continue.",
"api-help-param-no-description": "<span class=\"apihelp-empty\">(no description)</span>",
+ "api-help-param-maxbytes": "Cannot be longer than $1 {{PLURAL:$1|byte|bytes}}.",
+ "api-help-param-maxchars": "Cannot be longer than $1 {{PLURAL:$1|character|characters}}.",
"api-help-examples": "{{PLURAL:$1|Example|Examples}}:",
"api-help-permissions": "{{PLURAL:$1|Permission|Permissions}}:",
"api-help-permissions-granted-to": "{{PLURAL:$1|Granted to}}: $2",
"apierror-invalidurlparam": "Invalid value for <var>$1urlparam</var> (<kbd>$2=$3</kbd>).",
"apierror-invaliduser": "Invalid username \"$1\".",
"apierror-invaliduserid": "User ID <var>$1</var> is not valid.",
+ "apierror-maxbytes": "Parameter <var>$1</var> cannot be longer than $2 {{PLURAL:$2|byte|bytes}}",
+ "apierror-maxchars": "Parameter <var>$1</var> cannot be longer than $2 {{PLURAL:$2|character|characters}}",
"apierror-maxlag-generic": "Waiting for a database server: $1 {{PLURAL:$1|second|seconds}} lagged.",
"apierror-maxlag": "Waiting for $2: $1 {{PLURAL:$1|second|seconds}} lagged.",
"apierror-mimesearchdisabled": "MIME search is disabled in Miser Mode.",
"api-help-param-direction": "{{doc-apihelp-param|description=any standard \"dir\" parameter|noseealso=1}}",
"api-help-param-continue": "{{doc-apihelp-param|description=any standard \"continue\" parameter, or other parameter with the same semantics|noseealso=1}}",
"api-help-param-no-description": "Displayed on API parameters that lack any description",
+ "api-help-param-maxbytes": "Used to display the maximum allowed length of a parameter, in bytes.",
+ "api-help-param-maxchars": "Used to display the maximum allowed length of a parameter, in characters.",
"api-help-examples": "Label for the API help examples section\n\nParameters:\n* $1 - Number of examples to be displayed\n{{Identical|Example}}",
"api-help-permissions": "Label for the \"permissions\" section in the main module's help output.\n\nParameters:\n* $1 - Number of permissions displayed\n{{Identical|Permission}}",
"api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated",
"apierror-invalidurlparam": "{{doc-apierror}}\n\nParameters:\n* $1 - Module parameter prefix, e.g. \"bl\".\n* $2 - Key\n* $3 - Value.",
"apierror-invaliduser": "{{doc-apierror}}\n\nParameters:\n* $1 - User name that is invalid.",
"apierror-invaliduserid": "{{doc-apierror}}",
+ "apierror-maxbytes": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum allowed bytes.",
+ "apierror-maxchars": "{{doc-apierror}}\n\nParameters:\n* $1 - Parameter name.\n* $2 - Maximum allowed characters.",
"apierror-maxlag-generic": "{{doc-apierror}}\n\nParameters:\n* $1 - Database is lag in seconds.",
"apierror-maxlag": "{{doc-apierror}}\n\nParameters:\n* $1 - Database lag in seconds.\n* $2 - Database server that is lagged.",
"apierror-mimesearchdisabled": "{{doc-apierror}}",
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
-/** @deprecated use class constant instead */
-define( 'MW_DIFF_VERSION', '1.11a' );
-
/**
* @todo document
* @ingroup DifferenceEngine
* fixes important bugs or such to force cached diff views to
* clear.
*/
- const DIFF_VERSION = MW_DIFF_VERSION;
+ const DIFF_VERSION = '1.12';
/** @var int */
public $mOldid;
$key = false;
$cache = ObjectCache::getMainWANInstance();
if ( $this->mOldid && $this->mNewid ) {
+ // Check if subclass is still using the old way
+ // for backwards-compatibility
$key = $this->getDiffBodyCacheKey();
+ if ( $key === null ) {
+ $key = call_user_func_array(
+ [ $cache, 'makeKey' ],
+ $this->getDiffBodyCacheKeyParams()
+ );
+ }
// Try cache
if ( !$this->mRefreshCache ) {
$difftext = $cache->get( $key );
if ( $difftext ) {
wfIncrStats( 'diff_cache.hit' );
- $difftext = $this->localiseLineNumbers( $difftext );
+ $difftext = $this->localiseDiff( $difftext );
$difftext .= "\n<!-- diff cache key $key -->\n";
return $difftext;
} else {
wfIncrStats( 'diff_cache.uncacheable' );
}
- // Replace line numbers with the text in the user's language
+ // localise line numbers and title attribute text
if ( $difftext !== false ) {
- $difftext = $this->localiseLineNumbers( $difftext );
+ $difftext = $this->localiseDiff( $difftext );
}
return $difftext;
/**
* Returns the cache key for diff body text or content.
*
+ * @deprecated since 1.31, use getDiffBodyCacheKeyParams() instead
* @since 1.23
*
* @throws MWException
- * @return string
+ * @return string|null
*/
protected function getDiffBodyCacheKey() {
+ return null;
+ }
+
+ /**
+ * Get the cache key parameters
+ *
+ * Subclasses can replace the first element in the array to something
+ * more specific to the type of diff (e.g. "inline-diff"), or append
+ * if the cache should vary on more things. Overriding entirely should
+ * be avoided.
+ *
+ * @since 1.31
+ *
+ * @return array
+ * @throws MWException
+ */
+ protected function getDiffBodyCacheKeyParams() {
if ( !$this->mOldid || !$this->mNewid ) {
throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
}
- return wfMemcKey( 'diff', 'version', self::DIFF_VERSION,
- 'oldid', $this->mOldid, 'newid', $this->mNewid );
+ $engine = $this->getEngine();
+ $params = [
+ 'diff',
+ $engine,
+ self::DIFF_VERSION,
+ "old-{$this->mOldid}",
+ "rev-{$this->mNewid}"
+ ];
+
+ if ( $engine === 'wikidiff2' ) {
+ $params[] = phpversion( 'wikidiff2' );
+ $params[] = $this->getConfig()->get( 'WikiDiff2MovedParagraphDetectionCutoff' );
+ }
+
+ return $params;
}
/**
}
/**
- * Generates diff, to be wrapped internally in a logging/instrumentation
+ * Process $wgExternalDiffEngine and get a sane, usable engine
*
- * @param string $otext Old text, must be already segmented
- * @param string $ntext New text, must be already segmented
- * @return bool|string
- * @throws Exception
+ * @return bool|string 'wikidiff2', path to an executable, or false
*/
- protected function textDiff( $otext, $ntext ) {
- global $wgExternalDiffEngine, $wgContLang;
-
- $otext = str_replace( "\r\n", "\n", $otext );
- $ntext = str_replace( "\r\n", "\n", $ntext );
-
+ private function getEngine() {
+ global $wgExternalDiffEngine;
+ // We use the global here instead of Config because we write to the value,
+ // and Config is not mutable.
if ( $wgExternalDiffEngine == 'wikidiff' || $wgExternalDiffEngine == 'wikidiff3' ) {
wfDeprecated( "\$wgExternalDiffEngine = '{$wgExternalDiffEngine}'", '1.27' );
$wgExternalDiffEngine = false;
$wgExternalDiffEngine = false;
}
+ if ( is_string( $wgExternalDiffEngine ) && is_executable( $wgExternalDiffEngine ) ) {
+ return $wgExternalDiffEngine;
+ } elseif ( $wgExternalDiffEngine === false && function_exists( 'wikidiff2_do_diff' ) ) {
+ return 'wikidiff2';
+ } else {
+ // Native PHP
+ return false;
+ }
+ }
+
+ /**
+ * Generates diff, to be wrapped internally in a logging/instrumentation
+ *
+ * @param string $otext Old text, must be already segmented
+ * @param string $ntext New text, must be already segmented
+ * @return bool|string
+ */
+ protected function textDiff( $otext, $ntext ) {
+ global $wgContLang;
+
+ $otext = str_replace( "\r\n", "\n", $otext );
+ $ntext = str_replace( "\r\n", "\n", $ntext );
+
+ $engine = $this->getEngine();
+
// Better external diff engine, the 2 may some day be dropped
// This one does the escaping and segmenting itself
- if ( function_exists( 'wikidiff2_do_diff' ) && $wgExternalDiffEngine === false ) {
+ if ( $engine === 'wikidiff2' ) {
$wikidiff2Version = phpversion( 'wikidiff2' );
if (
$wikidiff2Version !== false &&
$text .= $this->debug( 'wikidiff2' );
return $text;
- } elseif ( $wgExternalDiffEngine !== false && is_executable( $wgExternalDiffEngine ) ) {
+ } elseif ( $engine !== false ) {
# Diff via the shell
$tmpDir = wfTempDir();
$tempName1 = tempnam( $tmpDir, 'diff_' );
fwrite( $tempFile2, $ntext );
fclose( $tempFile1 );
fclose( $tempFile2 );
- $cmd = [ $wgExternalDiffEngine, $tempName1, $tempName2 ];
+ $cmd = [ $engine, $tempName1, $tempName2 ];
$result = Shell::command( $cmd )
->execute();
$exitCode = $result->getExitCode();
);
}
$difftext = $result->getStdout();
- $difftext .= $this->debug( "external $wgExternalDiffEngine" );
+ $difftext .= $this->debug( "external $engine" );
unlink( $tempName1 );
unlink( $tempName2 );
" -->\n";
}
+ /**
+ * Localise diff output
+ *
+ * @param string $text
+ * @return string
+ */
+ private function localiseDiff( $text ) {
+ $text = $this->localiseLineNumbers( $text );
+ if ( $this->getEngine() === 'wikidiff2' &&
+ version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
+ ) {
+ $text = $this->addLocalisedTitleTooltips( $text );
+ }
+ return $text;
+ }
+
/**
* Replace line numbers with the text in the user's language
*
return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
}
+ /**
+ * Add title attributes for tooltips on moved paragraph indicators
+ *
+ * @param string $text
+ * @return string
+ */
+ private function addLocalisedTitleTooltips( $text ) {
+ return preg_replace_callback(
+ '/class="mw-diff-movedpara-(left|old)"/',
+ [ $this, 'addLocalisedTitleTooltipsCb' ],
+ $text
+ );
+ }
+
+ /**
+ * @param array $matches
+ * @return string
+ */
+ private function addLocalisedTitleTooltipsCb( array $matches ) {
+ $key = $matches[1] === 'right' ?
+ 'diff-paragraph-moved-toold' :
+ 'diff-paragraph-moved-tonew';
+ return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
+ }
+
/**
* If there are revisions between the ones being compared, return a note saying so.
*
);
if ( $charsLeft < 0 ) {
- return $keyspace . ':##' . md5( implode( ':', $args ) );
+ return $keyspace . ':BagOStuff-long-key:##' . md5( implode( ':', $args ) );
}
return $keyspace . ':' . implode( ':', $args );
"diff-multi-sameuser": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by the same user not shown)",
"diff-multi-otherusers": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by {{PLURAL:$2|one other user|$2 users}} not shown)",
"diff-multi-manyusers": "({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by more than $2 {{PLURAL:$2|user|users}} not shown)",
+ "diff-paragraph-moved-tonew": "Paragraph was moved. Click to jump to new location.",
+ "diff-paragraph-moved-toold": "Paragraph was moved. Click to jump to old location.",
"difference-missing-revision": "{{PLURAL:$2|One revision|$2 revisions}} of this difference ($1) {{PLURAL:$2|was|were}} not found.\n\nThis is usually caused by following an outdated diff link to a page that has been deleted.\nDetails can be found in the [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} deletion log].",
"search-summary": "",
"searchresults": "Search results",
"diff-multi-sameuser": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and the intermediate revisions were all created by the same user as the new revision.\n\nParameters:\n* $1 - the number of revisions\n{{Related|Diff-multi}}",
"diff-multi-otherusers": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and at least one of the intermediate revisions was created by a user other than the user who created the new revision.\n\nParameters:\n* $1 - the number of revisions\n* $2 - the number of distinct other users who made those revisions\n{{Related|Diff-multi}}",
"diff-multi-manyusers": "This message appears in the revision history of a page when comparing two versions which aren't consecutive, and the intermediate revisions have been edited by more than 100 users.\n\nParameters:\n* $1 - the number of revisions, will always be 101 or more\n* $2 - the number of users that were found, which was limited at 100\n{{Related|Diff-multi}}",
+ "diff-paragraph-moved-tonew": "Explaining title tag for the indicating symbols when a paragraph was moved hinting to the new location in the Diff view.",
+ "diff-paragraph-moved-toold": "Explaining title tag for the indicating symbols when a paragraph was moved hinting to the old location in the Diff view.",
"difference-missing-revision": "Text displayed when the requested revision does not exist using a diff link.\n\nExample: [{{canonicalurl:Project:News|diff=426850&oldid=99999999}} Diff with invalid revision#]\n\nParameters:\n* $1 - the list of missing revisions IDs\n* $2 - the number of items in $1 (one or two)",
"search-summary": "{{doc-specialpagesummary|search}}",
"searchresults": "This is the title of the page that contains the results of a search.\n\n{{Identical|Search results}}",
'api-help-param-integer-minmax',
'api-help-param-multi-separate',
'api-help-param-multi-max',
+ 'api-help-param-maxbytes',
+ 'api-help-param-maxchars',
'apisandbox-submit-invalid-fields-title',
'apisandbox-submit-invalid-fields-message',
'apisandbox-results',
} ) );
}
}
+ if ( 'maxbytes' in pi.parameters[ i ] ) {
+ descriptionContainer.append( $( '<div>', {
+ addClass: 'info',
+ append: Util.parseMsg( 'api-help-param-maxbytes', pi.parameters[ i ].maxbytes )
+ } ) );
+ }
+ if ( 'maxchars' in pi.parameters[ i ] ) {
+ descriptionContainer.append( $( '<div>', {
+ addClass: 'info',
+ append: Util.parseMsg( 'api-help-param-maxchars', pi.parameters[ i ].maxchars )
+ } ) );
+ }
helpField = new OO.ui.FieldLayout(
new OO.ui.Widget( {
$content: '\xa0',
);
$this->assertEquals(
- 'test:##dc89dcb43b28614da27660240af478b5',
+ 'test:BagOStuff-long-key:##dc89dcb43b28614da27660240af478b5',
$this->cache->makeKey( '𝕖𝕧𝕖𝕟', '𝕚𝕗', '𝕨𝕖', '𝕄𝔻𝟝', '𝕖𝕒𝕔𝕙',
'𝕒𝕣𝕘𝕦𝕞𝕖𝕟𝕥', '𝕥𝕙𝕚𝕤', '𝕜𝕖𝕪', '𝕨𝕠𝕦𝕝𝕕', '𝕤𝕥𝕚𝕝𝕝', '𝕓𝕖', '𝕥𝕠𝕠', '𝕝𝕠𝕟𝕘' )
);
foreach ( [ $paramsPlain, $paramsForHelp ] as $params ) {
foreach ( $params as $param => $config ) {
- if (
- isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] )
+ if ( isset( $config[ApiBase::PARAM_ISMULTI_LIMIT1] )
|| isset( $config[ApiBase::PARAM_ISMULTI_LIMIT2] )
) {
$this->assertTrue( !empty( $config[ApiBase::PARAM_ISMULTI] ), $param
$config[ApiBase::PARAM_ISMULTI_LIMIT2], $param
. 'PARAM_ISMULTI limit cannot be smaller for users with apihighlimits rights' );
}
+ if ( isset( $config[ApiBase::PARAM_MAX_BYTES] )
+ || isset( $config[ApiBase::PARAM_MAX_CHARS] )
+ ) {
+ $default = isset( $config[ApiBase::PARAM_DFLT] ) ? $config[ApiBase::PARAM_DFLT] : null;
+ $type = isset( $config[ApiBase::PARAM_TYPE] ) ? $config[ApiBase::PARAM_TYPE]
+ : gettype( $default );
+ $this->assertContains( $type, [ 'NULL', 'string', 'text', 'password' ],
+ 'PARAM_MAX_BYTES/CHARS is only supported for string-like types' );
+ }
}
}
}