wiki.techinc.nl/includes/libs/ParamValidator/ParamValidator.php
Brad Jorsch aa0720d37c ParamValidator: Use MessageValue!
Trying to get away with returning a single code and parameter-list that
was supposed to represent both human-readable and machine-readable data
was a mistake.

This patch converts it to use DataMessageValue, which represents the two
separately and also provides guidance for supplying translations of all
the error codes.

This also eliminates the "describeSettings()" method that was trying to
serve multiple use cases (in terms of the Action API, action=paraminfo
and action=help). It's replaced by two methods that each serve one of
the use cases. Also some of the functionality was moved out of the
TypeDef base class into ParamValidator, to better match where the
constants themselves live.

Also I wound up creating a NumericDef base class so FloatDef can share
the same range-checking logic that IntegerDef has. I probably should
have done that as a separate patch, but untangling it now would be too
much work.

Bug: T235801
Change-Id: Iea6d4a1d05bb4b92d60415b0f03ff9d3dc99a80b
2019-11-01 15:49:31 -04:00

714 lines
23 KiB
PHP

<?php
namespace Wikimedia\ParamValidator;
use DomainException;
use InvalidArgumentException;
use Wikimedia\Assert\Assert;
use Wikimedia\Message\DataMessageValue;
use Wikimedia\Message\MessageValue;
use Wikimedia\Message\ParamType;
use Wikimedia\Message\ScalarParam;
use Wikimedia\ObjectFactory;
/**
* Service for formatting and validating API parameters
*
* A settings array is simply an array with keys being the relevant PARAM_*
* constants from this class, TypeDef, and its subclasses.
*
* As a general overview of the architecture here:
* - ParamValidator handles some general validation of the parameter,
* then hands off to a TypeDef subclass to validate the specific representation
* based on the parameter's type.
* - TypeDef subclasses handle conversion between the string representation
* submitted by the client and the output PHP data types, validating that the
* strings are valid representations of the intended type as they do so.
* - ValidationException is used to report fatal errors in the validation back
* to the caller, since the return value represents the successful result of
* the validation and might be any type or class.
* - The Callbacks interface allows ParamValidator to reach out and fetch data
* it needs to perform the validation. Currently that includes:
* - Fetching the value of the parameter being validated (largely since a generic
* caller cannot know whether it needs to fetch a string from $_GET/$_POST or
* an array from $_FILES).
* - Reporting of non-fatal warnings back to the caller.
* - Fetching the "high limits" flag when necessary, to avoid the need for loading
* the user unnecessarily.
*
* @since 1.34
* @unstable
*/
class ParamValidator {
/**
* @name Constants for parameter settings arrays
* These constants are keys in the settings array that define how the
* parameters coming in from the request are to be interpreted.
*
* If a constant is associated with a failure code, the failure code
* and data are described. ValidationExceptions are typically thrown, but
* those indicated as "non-fatal" are instead passed to
* Callbacks::recordCondition().
*
* Additional constants may be defined by TypeDef subclasses, or by other
* libraries for controlling things like auto-generated parameter documentation.
* For purposes of namespacing the constants, the values of all constants
* defined by this library begin with 'param-'.
*
* @{
*/
/** (mixed) Default value of the parameter. If omitted, null is the default. */
const PARAM_DEFAULT = 'param-default';
/**
* (string|array) Type of the parameter.
* Must be a registered type or an array of enumerated values (in which case the "enum"
* type must be registered). If omitted, the default is the PHP type of the default value
* (see PARAM_DEFAULT).
*/
const PARAM_TYPE = 'param-type';
/**
* (bool) Indicate that the parameter is required.
*
* Failure codes:
* - 'missingparam': The parameter is omitted/empty (and no default was set). No data.
*/
const PARAM_REQUIRED = 'param-required';
/**
* (bool) Indicate that the parameter is multi-valued.
*
* A multi-valued parameter may be submitted in one of several formats. All
* of the following result in a value of `[ 'a', 'b', 'c' ]`.
* - "a|b|c", i.e. pipe-separated.
* - "\x1Fa\x1Fb\x1Fc", i.e. separated by U+001F, with a signalling U+001F at the start.
* - As a string[], e.g. from a query string like "foo[]=a&foo[]=b&foo[]=c".
*
* Each of the multiple values is passed individually to the TypeDef.
* $options will contain a 'values-list' key holding the entire list.
*
* By default duplicates are removed from the resulting parameter list. Use
* PARAM_ALLOW_DUPLICATES to override that behavior.
*
* Failure codes:
* - 'toomanyvalues': More values were supplied than are allowed. See
* PARAM_ISMULTI_LIMIT1, PARAM_ISMULTI_LIMIT2, and constructor option
* 'ismultiLimits'. Data:
* - 'limit': The limit currently in effect.
* - 'lowlimit': The limit when high limits are not allowed.
* - 'highlimit': The limit when high limits are allowed.
* - 'unrecognizedvalues': Non-fatal. Invalid values were passed and
* PARAM_IGNORE_INVALID_VALUES was set. Data:
* - 'values': The unrecognized values.
*/
const PARAM_ISMULTI = 'param-ismulti';
/**
* (int) Maximum number of multi-valued parameter values allowed
*
* @see PARAM_ISMULTI
*/
const PARAM_ISMULTI_LIMIT1 = 'param-ismulti-limit1';
/**
* (int) Maximum number of multi-valued parameter values allowed for users
* allowed high limits.
*
* @see PARAM_ISMULTI
*/
const PARAM_ISMULTI_LIMIT2 = 'param-ismulti-limit2';
/**
* (bool|string) Whether a magic "all values" value exists for multi-valued
* enumerated types, and if so what that value is.
*
* When PARAM_TYPE has a defined set of values and PARAM_ISMULTI is true,
* this allows for an asterisk ('*') to be passed in place of a pipe-separated list of
* every possible value. If a string is set, it will be used in place of the asterisk.
*/
const PARAM_ALL = 'param-all';
/**
* (bool) Allow the same value to be set more than once when PARAM_ISMULTI is true?
*
* If not truthy, the set of values will be passed through
* `array_values( array_unique() )`. The default is falsey.
*/
const PARAM_ALLOW_DUPLICATES = 'param-allow-duplicates';
/**
* (bool) Indicate that the parameter's value should not be logged.
*
* Failure codes: (non-fatal)
* - 'param-sensitive': Always recorded when the parameter is used.
*/
const PARAM_SENSITIVE = 'param-sensitive';
/**
* (bool) Indicate that a deprecated parameter was used.
*
* Failure codes: (non-fatal)
* - 'param-deprecated': Always recorded when the parameter is used.
*/
const PARAM_DEPRECATED = 'param-deprecated';
/**
* (bool) Whether to ignore invalid values.
*
* This controls whether certain failures are considered fatal
* or non-fatal. The default is false.
*
* @see PARAM_ISMULTI
*/
const PARAM_IGNORE_INVALID_VALUES = 'param-ignore-invalid-values';
/** @} */
/** Magic "all values" value when PARAM_ALL is true. */
const ALL_DEFAULT_STRING = '*';
/** A list of standard type names and types that may be passed as `$typeDefs` to __construct(). */
public static $STANDARD_TYPES = [
'boolean' => [ 'class' => TypeDef\BooleanDef::class ],
'checkbox' => [ 'class' => TypeDef\PresenceBooleanDef::class ],
'integer' => [ 'class' => TypeDef\IntegerDef::class ],
'limit' => [ 'class' => TypeDef\LimitDef::class ],
'float' => [ 'class' => TypeDef\FloatDef::class ],
'double' => [ 'class' => TypeDef\FloatDef::class ],
'string' => [ 'class' => TypeDef\StringDef::class ],
'password' => [ 'class' => TypeDef\PasswordDef::class ],
'NULL' => [
'class' => TypeDef\StringDef::class,
'args' => [ [
'allowEmptyWhenRequired' => true,
] ],
],
'timestamp' => [ 'class' => TypeDef\TimestampDef::class ],
'upload' => [ 'class' => TypeDef\UploadDef::class ],
'enum' => [ 'class' => TypeDef\EnumDef::class ],
];
/** @var Callbacks */
private $callbacks;
/** @var ObjectFactory */
private $objectFactory;
/** @var (TypeDef|array)[] Map parameter type names to TypeDef objects or ObjectFactory specs */
private $typeDefs = [];
/** @var int Default values for PARAM_ISMULTI_LIMIT1 */
private $ismultiLimit1;
/** @var int Default values for PARAM_ISMULTI_LIMIT2 */
private $ismultiLimit2;
/**
* @param Callbacks $callbacks
* @param ObjectFactory $objectFactory To turn specs into TypeDef objects
* @param array $options Associative array of additional settings
* - 'typeDefs': (array) As for addTypeDefs(). If omitted, self::$STANDARD_TYPES will be used.
* Pass an empty array if you want to start with no registered types.
* - 'ismultiLimits': (int[]) Two ints, being the default values for PARAM_ISMULTI_LIMIT1 and
* PARAM_ISMULTI_LIMIT2. If not given, defaults to `[ 50, 500 ]`.
*/
public function __construct(
Callbacks $callbacks,
ObjectFactory $objectFactory,
array $options = []
) {
$this->callbacks = $callbacks;
$this->objectFactory = $objectFactory;
$this->addTypeDefs( $options['typeDefs'] ?? self::$STANDARD_TYPES );
$this->ismultiLimit1 = $options['ismultiLimits'][0] ?? 50;
$this->ismultiLimit2 = $options['ismultiLimits'][1] ?? 500;
}
/**
* List known type names
* @return string[]
*/
public function knownTypes() {
return array_keys( $this->typeDefs );
}
/**
* Register multiple type handlers
*
* @see addTypeDef()
* @param array $typeDefs Associative array mapping `$name` to `$typeDef`.
*/
public function addTypeDefs( array $typeDefs ) {
foreach ( $typeDefs as $name => $def ) {
$this->addTypeDef( $name, $def );
}
}
/**
* Register a type handler
*
* To allow code to omit PARAM_TYPE in settings arrays to derive the type
* from PARAM_DEFAULT, it is strongly recommended that the following types be
* registered: "boolean", "integer", "double", "string", "NULL", and "enum".
*
* When using ObjectFactory specs, the following extra arguments are passed:
* - The Callbacks object for this ParamValidator instance.
*
* @param string $name Type name
* @param TypeDef|array $typeDef Type handler or ObjectFactory spec to create one.
*/
public function addTypeDef( $name, $typeDef ) {
Assert::parameterType(
implode( '|', [ TypeDef::class, 'array' ] ),
$typeDef,
'$typeDef'
);
if ( isset( $this->typeDefs[$name] ) ) {
throw new InvalidArgumentException( "Type '$name' is already registered" );
}
$this->typeDefs[$name] = $typeDef;
}
/**
* Register a type handler, overriding any existing handler
* @see addTypeDef
* @param string $name Type name
* @param TypeDef|array|null $typeDef As for addTypeDef, or null to unregister a type.
*/
public function overrideTypeDef( $name, $typeDef ) {
Assert::parameterType(
implode( '|', [ TypeDef::class, 'array', 'null' ] ),
$typeDef,
'$typeDef'
);
if ( $typeDef === null ) {
unset( $this->typeDefs[$name] );
} else {
$this->typeDefs[$name] = $typeDef;
}
}
/**
* Test if a type is registered
* @param string $name Type name
* @return bool
*/
public function hasTypeDef( $name ) {
return isset( $this->typeDefs[$name] );
}
/**
* Get the TypeDef for a type
* @param string|array $type Any array is considered equivalent to the string "enum".
* @return TypeDef|null
*/
public function getTypeDef( $type ) {
if ( is_array( $type ) ) {
$type = 'enum';
}
if ( !isset( $this->typeDefs[$type] ) ) {
return null;
}
$def = $this->typeDefs[$type];
if ( !$def instanceof TypeDef ) {
$def = $this->objectFactory->createObject( $def, [
'extraArgs' => [ $this->callbacks ],
'assertClass' => TypeDef::class,
] );
$this->typeDefs[$type] = $def;
}
return $def;
}
/**
* Normalize a parameter settings array
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @return array
*/
public function normalizeSettings( $settings ) {
// Shorthand
if ( !is_array( $settings ) ) {
$settings = [
self::PARAM_DEFAULT => $settings,
];
}
// When type is not given, determine it from the type of the PARAM_DEFAULT
if ( !isset( $settings[self::PARAM_TYPE] ) ) {
$settings[self::PARAM_TYPE] = gettype( $settings[self::PARAM_DEFAULT] ?? null );
}
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
if ( $typeDef ) {
$settings = $typeDef->normalizeSettings( $settings );
}
return $settings;
}
/**
* Fetch and valiate a parameter value using a settings array
*
* @param string $name Parameter name
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array, passed through to the TypeDef and Callbacks.
* @return mixed Validated parameter value
* @throws ValidationException if the value is invalid
*/
public function getValue( $name, $settings, array $options = [] ) {
$settings = $this->normalizeSettings( $settings );
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
if ( !$typeDef ) {
throw new DomainException(
"Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
);
}
$value = $typeDef->getValue( $name, $settings, $options );
if ( $value !== null ) {
if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
$this->callbacks->recordCondition(
DataMessageValue::new( 'paramvalidator-param-sensitive', [], 'param-sensitive' )
->plaintextParams( $name, $value ),
$name, $value, $settings, $options
);
}
// Set a warning if a deprecated parameter has been passed
if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
$this->callbacks->recordCondition(
DataMessageValue::new( 'paramvalidator-param-deprecated', [], 'param-deprecated' )
->plaintextParams( $name, $value ),
$name, $value, $settings, $options
);
}
} elseif ( isset( $settings[self::PARAM_DEFAULT] ) ) {
$value = $settings[self::PARAM_DEFAULT];
}
return $this->validateValue( $name, $value, $settings, $options );
}
/**
* Valiate a parameter value using a settings array
*
* @param string $name Parameter name
* @param null|mixed $value Parameter value
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array, passed through to the TypeDef and Callbacks.
* - An additional option, 'values-list', will be set when processing the
* values of a multi-valued parameter.
* @return mixed Validated parameter value(s)
* @throws ValidationException if the value is invalid
*/
public function validateValue( $name, $value, $settings, array $options = [] ) {
$settings = $this->normalizeSettings( $settings );
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
if ( !$typeDef ) {
throw new DomainException(
"Param $name's type is unknown - {$settings[self::PARAM_TYPE]}"
);
}
if ( $value === null ) {
if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
throw new ValidationException(
DataMessageValue::new( 'paramvalidator-missingparam', [], 'missingparam' )
->plaintextParams( $name ),
$name, $value, $settings
);
}
return null;
}
// Non-multi
if ( empty( $settings[self::PARAM_ISMULTI] ) ) {
if ( substr( $value, 0, 1 ) === "\x1f" ) {
throw new ValidationException(
DataMessageValue::new( 'paramvalidator-notmulti', [], 'badvalue' )
->plaintextParams( $name, $value ),
$name, $value, $settings
);
}
return $typeDef->validate( $name, $value, $settings, $options );
}
// Split the multi-value and validate each parameter
$limit1 = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
$limit2 = max( $limit1, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
$valuesList = is_array( $value ) ? $value : self::explodeMultiValue( $value, $limit2 + 1 );
// Handle PARAM_ALL
$enumValues = $typeDef->getEnumValues( $name, $settings, $options );
if ( is_array( $enumValues ) && isset( $settings[self::PARAM_ALL] ) &&
count( $valuesList ) === 1
) {
$allValue = is_string( $settings[self::PARAM_ALL] )
? $settings[self::PARAM_ALL]
: self::ALL_DEFAULT_STRING;
if ( $valuesList[0] === $allValue ) {
return $enumValues;
}
}
// Avoid checking useHighLimits() unless it's actually necessary
$sizeLimit = (
$limit2 > $limit1 && count( $valuesList ) > $limit1 &&
$this->callbacks->useHighLimits( $options )
) ? $limit2 : $limit1;
if ( count( $valuesList ) > $sizeLimit ) {
if ( is_array( $value ) ) {
$value = self::implodeMultiValue( $value );
}
throw new ValidationException(
DataMessageValue::new( 'paramvalidator-toomanyvalues', [], 'toomanyvalues', [
'limit' => $sizeLimit,
'lowlimit' => $limit1,
'highlimit' => $limit2,
] )->plaintextParams( $name, $value )->numParams( $sizeLimit ),
$name, $valuesList, $settings
);
}
$options['values-list'] = $valuesList;
$validValues = [];
$invalidValues = [];
foreach ( $valuesList as $v ) {
try {
$validValues[] = $typeDef->validate( $name, $v, $settings, $options );
} catch ( ValidationException $ex ) {
if ( empty( $settings[self::PARAM_IGNORE_INVALID_VALUES] ) ) {
throw $ex;
}
$invalidValues[] = $v;
}
}
if ( $invalidValues ) {
$this->callbacks->recordCondition(
DataMessageValue::new( 'paramvalidator-unrecognizedvalues', [], 'unrecognizedvalues', [
'values' => $invalidValues,
] )
->plaintextParams( $name, $value )
->commaListParams( array_map( function ( $v ) {
return new ScalarParam( ParamType::PLAINTEXT, $v );
}, $invalidValues ) )
->numParams( count( $invalidValues ) ),
$name, $value, $settings, $options
);
}
// Throw out duplicates if requested
if ( empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
$validValues = array_values( array_unique( $validValues ) );
}
return $validValues;
}
/**
* Describe parameter settings in a machine-readable format.
*
* @param string $name Parameter name.
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array.
* @return array
*/
public function getParamInfo( $name, $settings, array $options ) {
$settings = $this->normalizeSettings( $settings );
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
$info = [];
$info['type'] = $settings[self::PARAM_TYPE];
$info['required'] = !empty( $settings[self::PARAM_REQUIRED] );
if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
$info['deprecated'] = true;
}
if ( !empty( $settings[self::PARAM_SENSITIVE] ) ) {
$info['sensitive'] = true;
}
if ( isset( $settings[self::PARAM_DEFAULT] ) ) {
$info['default'] = $settings[self::PARAM_DEFAULT];
}
$info['multi'] = !empty( $settings[self::PARAM_ISMULTI] );
if ( $info['multi'] ) {
$info['lowlimit'] = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
$info['highlimit'] = max(
$info['lowlimit'], $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2
);
$info['limit'] =
$info['highlimit'] > $info['lowlimit'] && $this->callbacks->useHighLimits( $options )
? $info['highlimit']
: $info['lowlimit'];
if ( !empty( $settings[self::PARAM_ALLOW_DUPLICATES] ) ) {
$info['allowsduplicates'] = true;
}
$allSpecifier = $settings[self::PARAM_ALL] ?? false;
if ( $allSpecifier !== false ) {
if ( !is_string( $allSpecifier ) ) {
$allSpecifier = self::ALL_DEFAULT_STRING;
}
$info['allspecifier'] = $allSpecifier;
}
}
if ( $typeDef ) {
$info = array_merge( $info, $typeDef->getParamInfo( $name, $settings, $options ) );
}
// Filter out nulls (strictly)
return array_filter( $info, function ( $v ) {
return $v !== null;
} );
}
/**
* Describe parameter settings in human-readable format
*
* @param string $name Parameter name being described.
* @param array|mixed $settings Default value or an array of settings
* using PARAM_* constants.
* @param array $options Options array.
* @return MessageValue[]
*/
public function getHelpInfo( $name, $settings, array $options ) {
$settings = $this->normalizeSettings( $settings );
$typeDef = $this->getTypeDef( $settings[self::PARAM_TYPE] );
// Define ordering. Some are overwritten below, some expected from the TypeDef
$info = [
self::PARAM_DEPRECATED => null,
self::PARAM_REQUIRED => null,
self::PARAM_SENSITIVE => null,
self::PARAM_TYPE => null,
self::PARAM_ISMULTI => null,
self::PARAM_ISMULTI_LIMIT1 => null,
self::PARAM_ALL => null,
self::PARAM_DEFAULT => null,
];
if ( !empty( $settings[self::PARAM_DEPRECATED] ) ) {
$info[self::PARAM_DEPRECATED] = MessageValue::new( 'paramvalidator-help-deprecated' );
}
if ( !empty( $settings[self::PARAM_REQUIRED] ) ) {
$info[self::PARAM_REQUIRED] = MessageValue::new( 'paramvalidator-help-required' );
}
if ( !empty( $settings[self::PARAM_ISMULTI] ) ) {
$info[self::PARAM_ISMULTI] = MessageValue::new( 'paramvalidator-help-multi-sep' );
$lowcount = $settings[self::PARAM_ISMULTI_LIMIT1] ?? $this->ismultiLimit1;
$highcount = max( $lowcount, $settings[self::PARAM_ISMULTI_LIMIT2] ?? $this->ismultiLimit2 );
$values = $typeDef ? $typeDef->getEnumValues( $name, $settings, $options ) : null;
if (
// Only mention the limits if they're likely to matter.
$values === null || count( $values ) > $lowcount ||
!empty( $settings[self::PARAM_ALLOW_DUPLICATES] )
) {
if ( $highcount > $lowcount ) {
$info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max' )
->numParams( $lowcount, $highcount );
} else {
$info[self::PARAM_ISMULTI_LIMIT1] = MessageValue::new( 'paramvalidator-help-multi-max-simple' )
->numParams( $lowcount );
}
}
$allSpecifier = $settings[self::PARAM_ALL] ?? false;
if ( $allSpecifier !== false ) {
if ( !is_string( $allSpecifier ) ) {
$allSpecifier = self::ALL_DEFAULT_STRING;
}
$info[self::PARAM_ALL] = MessageValue::new( 'paramvalidator-help-multi-all' )
->plaintextParams( $allSpecifier );
}
}
if ( isset( $settings[self::PARAM_DEFAULT] ) && $typeDef ) {
$value = $typeDef->stringifyValue( $name, $settings[self::PARAM_DEFAULT], $settings, $options );
if ( $value === '' ) {
$info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-param-default-empty' );
} elseif ( $value !== null ) {
$info[self::PARAM_DEFAULT] = MessageValue::new( 'paramvalidator-param-default' )
->plaintextParams( $value );
}
}
if ( $typeDef ) {
$info = array_merge( $info, $typeDef->getHelpInfo( $name, $settings, $options ) );
}
// Put the default at the very end (the TypeDef may have added extra messages)
$default = $info[self::PARAM_DEFAULT];
unset( $info[self::PARAM_DEFAULT] );
$info[self::PARAM_DEFAULT] = $default;
// Filter out nulls
return array_filter( $info );
}
/**
* Split a multi-valued parameter string, like explode()
*
* Note that, unlike explode(), this will return an empty array when given
* an empty string.
*
* @param string $value
* @param int $limit
* @return string[]
*/
public static function explodeMultiValue( $value, $limit ) {
if ( $value === '' || $value === "\x1f" ) {
return [];
}
if ( substr( $value, 0, 1 ) === "\x1f" ) {
$sep = "\x1f";
$value = substr( $value, 1 );
} else {
$sep = '|';
}
return explode( $sep, $value, $limit );
}
/**
* Implode an array as a multi-valued parameter string, like implode()
*
* @param array $value
* @return string
*/
public static function implodeMultiValue( array $value ) {
if ( $value === [ '' ] ) {
// There's no value that actually returns a single empty string.
// Best we can do is this that returns two, which will be deduplicated to one.
return '|';
}
foreach ( $value as $v ) {
if ( strpos( $v, '|' ) !== false ) {
return "\x1f" . implode( "\x1f", $value );
}
}
return implode( '|', $value );
}
}