From 17d001b73a3bd291e0f77794fa60fa5495609d68 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Gerg=C5=91=20Tisza?= Date: Sun, 12 Nov 2017 09:51:34 +0000 Subject: [PATCH] Add string length limits Adds two new ApiBase::getAllowedParams() keys: PARAM_MAX_BYTES and PARAM_MAX_CHARS, to set a length limit for a (string-like) parameter. This makes it easy to document and enforce database field length limits (where relying on the database would either result in unfriendly error messages or silent truncation, depending on DB settings) and also exposes them in structured form so API clients can verify the length without doing roundtrips. Change-Id: I2e784972d7e11cad79fdef887bbcde297dbd9ce0 --- includes/api/ApiBase.php | 33 ++++++++++++++++++-- includes/api/ApiHelp.php | 9 ++++++ includes/api/ApiParamInfo.php | 6 ++++ includes/api/i18n/en.json | 4 +++ includes/api/i18n/qqq.json | 4 +++ tests/phpunit/structure/ApiStructureTest.php | 12 +++++-- 6 files changed, 64 insertions(+), 4 deletions(-) diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 80aeff5478..cb3c2f6da7 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -217,6 +217,18 @@ abstract class ApiBase extends ContextSource { */ 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 = '*'; @@ -1173,9 +1185,9 @@ abstract class ApiBase extends ContextSource { ); } - // 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 @@ -1285,6 +1297,23 @@ abstract class ApiBase extends ContextSource { $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; diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 318555a93b..02404c4250 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -711,6 +711,15 @@ class ApiHelp extends ApiBase { } } + 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] diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 2fa20a96ea..93fc51a7c6 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -471,6 +471,12 @@ class ApiParamInfo extends ApiBase { 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'] ) ) { diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index dbd5451409..85f17debc2 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -1605,6 +1605,8 @@ "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": "(no description)", + "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", @@ -1710,6 +1712,8 @@ "apierror-invalidurlparam": "Invalid value for $1urlparam ($2=$3).", "apierror-invaliduser": "Invalid username \"$1\".", "apierror-invaliduserid": "User ID $1 is not valid.", + "apierror-maxbytes": "Parameter $1 cannot be longer than $2 {{PLURAL:$2|byte|bytes}}", + "apierror-maxchars": "Parameter $1 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.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index 6aaaac70f6..3bdf7c6d1d 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -1496,6 +1496,8 @@ "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", @@ -1599,6 +1601,8 @@ "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}}", diff --git a/tests/phpunit/structure/ApiStructureTest.php b/tests/phpunit/structure/ApiStructureTest.php index 7912f97902..cbc74f4ed7 100644 --- a/tests/phpunit/structure/ApiStructureTest.php +++ b/tests/phpunit/structure/ApiStructureTest.php @@ -182,8 +182,7 @@ class ApiStructureTest extends MediaWikiTestCase { 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 @@ -199,6 +198,15 @@ class ApiStructureTest extends MediaWikiTestCase { $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' ); + } } } } -- 2.20.1