mediawiki-extensions-Gadgets/includes/Content/GadgetDefinitionValidator.php
Timo Tijhof d2a37f5f69 Replace EditFilterMergedContent hook with ContentHandler override
The reason for this hook is not the validation itself, because that
is already done by `GadgetDefinitionContent->isValid` which is part
of the core Content interface, already enforced by ContentHandler.

Instead, the hook was here to provide the custom interface message
GadgetDefinitionValidator, because the core Content interface is
limited to boolean isValid(), which provides a very generic error
message.

However, nowadays ContentHandler exposes this mechanism directly
such that we can directly attach a custom message to it without
needing to wait for the stack to reach the EditPage and then override
it after the fact from a global hook.

Also:

* Simplify validation logic towards "is" checks with only an
  expected description.

* Move schema.json file to top-level file.
  It has been unused for as long as it has been in the repo, despite
  appearing (due to its placement) to be used as part of the source.
  It was added, I believe, with the intent to be used by the validator,
  but it isn't. It also isn't validated or checked for correctness
  by anything right now.
  For now, keep it as informal schema in the top-level location for
  easy discovery where perhaps others can find a use for it.
  SD0001 mentions gadget developers may want to start using it for
  Git-maintained gadgets to help with validation in their IDE, after
  Gadgets 2.0 is launched.

Test Plan:
* Set `$wgGadgetsRepo = 'json+definition';`
* Create `MediaWiki:Gadgets/example.json`
* Attempt to save "x" in settings.namespaces item.
* Attempt to save "x.zip" in module.pages item.
* Fails with this patch, similar as on master.

Bug: T31272
Change-Id: I61bc3e40348a0aeb3bd3fa9ca86ccb7b93304095
2024-04-24 19:31:14 +00:00

116 lines
4 KiB
PHP

<?php
namespace MediaWiki\Extension\Gadgets\Content;
use MediaWiki\Status\Status;
/**
* Validate the content of a gadget definition page.
*
* @see MediaWikiGadgetsJsonRepo
* @see GadgetDefinitionContent
* @internal
*/
class GadgetDefinitionValidator {
private const TYPE_ARRAY = [ 'callback' => 'is_array', 'expect' => 'array' ];
private const TYPE_BOOL = [ 'callback' => 'is_bool', 'expect' => 'boolean' ];
private const TYPE_INT = [ 'callback' => 'is_int', 'expect' => 'number' ];
private const TYPE_STRING = [ 'callback' => 'is_string', 'expect' => 'string' ];
private const TYPE_PAGE_SUFFIX = [
'callback' => [ __CLASS__, 'isResourcePageSuffix' ], 'expect' => '.js, .css, .json'
];
private const TYPE_MODULE_TYPE = [
'callback' => [ __CLASS__, 'isModuleType' ], 'expect' => '"", "general", "styles"',
];
/**
* @var array Schema for the definition.
*
* - callback: boolean check.
* - expect: human-readable description for the gadgets-validate-wrongtype message.
* - child: optional type+expect for each array item.
*/
protected static $schema = [
'settings' => self::TYPE_ARRAY,
'settings.actions' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'settings.categories' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'settings.category' => self::TYPE_STRING,
'settings.contentModels' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'settings.default' => self::TYPE_BOOL,
'settings.hidden' => self::TYPE_BOOL,
'settings.namespaces' => self::TYPE_ARRAY + [ 'child' => self::TYPE_INT ],
'settings.package' => self::TYPE_BOOL,
'settings.requiresES6' => self::TYPE_BOOL,
'settings.rights' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'settings.skins' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'settings.supportsUrlLoad' => self::TYPE_BOOL,
'module' => self::TYPE_ARRAY,
'module.dependencies' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'module.messages' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'module.pages' => self::TYPE_ARRAY + [ 'child' => self::TYPE_PAGE_SUFFIX ],
'module.peers' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ],
'module.type' => self::TYPE_MODULE_TYPE,
];
public static function isResourcePageSuffix( $title ): bool {
return is_string( $title ) && (
str_ends_with( $title, '.js' ) || str_ends_with( $title, '.css' ) || str_ends_with( $title, '.json' )
);
}
public static function isModuleType( string $type ): bool {
return $type === '' || $type === 'general' || $type === 'styles';
}
/**
* Check the validity of known properties in a gadget definition array.
*
* @param array $definition
* @param bool $tolerateMissing If true, don't complain about missing keys
* @return Status object with error message if applicable
*/
public function validate( array $definition, $tolerateMissing = false ) {
foreach ( self::$schema as $property => $validation ) {
// Access the property by walking from the root to the specified property
$val = $definition;
foreach ( explode( '.', $property ) as $propName ) {
if ( !array_key_exists( $propName, $val ) ) {
if ( $tolerateMissing ) {
// Skip validation of this property altogether
continue 2;
}
return Status::newFatal( 'gadgets-validate-notset', $property );
}
$val = $val[$propName];
}
// Validate this property
$isValid = ( $validation['callback'] )( $val );
if ( !$isValid ) {
return Status::newFatal(
'gadgets-validate-wrongtype', $property,
$validation['expect']
);
}
// Descend into the array and validate each array item
if ( isset( $validation['child'] ) && is_array( $val ) ) {
foreach ( $val as $key => $item ) {
$isValid = ( $validation['child']['callback'] )( $item );
if ( !$isValid ) {
return Status::newFatal(
'gadgets-validate-wrongtype',
"{$property}[{$key}]",
$validation['child']['expect']
);
}
}
}
}
return Status::newGood();
}
}