Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 31 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
| GadgetDefinitionValidator | |
0.00% |
0 / 31 |
|
0.00% |
0 / 3 |
342 | |
0.00% |
0 / 1 |
| isResourcePageSuffix | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
| isModuleType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
12 | |||
| validate | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
110 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Extension\Gadgets\Content; |
| 4 | |
| 5 | use MediaWiki\Status\Status; |
| 6 | |
| 7 | /** |
| 8 | * Validate the content of a gadget definition page. |
| 9 | * |
| 10 | * @see MediaWikiGadgetsJsonRepo |
| 11 | * @see GadgetDefinitionContent |
| 12 | * @internal |
| 13 | */ |
| 14 | class GadgetDefinitionValidator { |
| 15 | private const TYPE_ARRAY = [ 'callback' => 'is_array', 'expect' => 'array' ]; |
| 16 | private const TYPE_BOOL = [ 'callback' => 'is_bool', 'expect' => 'boolean' ]; |
| 17 | private const TYPE_INT = [ 'callback' => 'is_int', 'expect' => 'number' ]; |
| 18 | private const TYPE_STRING = [ 'callback' => 'is_string', 'expect' => 'string' ]; |
| 19 | private const TYPE_PAGE_SUFFIX = [ |
| 20 | 'callback' => [ self::class, 'isResourcePageSuffix' ], 'expect' => '.js, .css, .json, .vue' |
| 21 | ]; |
| 22 | private const TYPE_MODULE_TYPE = [ |
| 23 | 'callback' => [ self::class, 'isModuleType' ], 'expect' => '"", "general", "styles"', |
| 24 | ]; |
| 25 | |
| 26 | /** |
| 27 | * @var array Schema for the definition. |
| 28 | * |
| 29 | * - callback: boolean check. |
| 30 | * - expect: human-readable description for the gadgets-validate-wrongtype message. |
| 31 | * - child: optional type+expect for each array item. |
| 32 | */ |
| 33 | protected static $schema = [ |
| 34 | 'settings' => self::TYPE_ARRAY, |
| 35 | 'settings.actions' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 36 | 'settings.categories' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 37 | 'settings.section' => self::TYPE_STRING, |
| 38 | 'settings.contentModels' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 39 | 'settings.default' => self::TYPE_BOOL, |
| 40 | 'settings.hidden' => self::TYPE_BOOL, |
| 41 | 'settings.namespaces' => self::TYPE_ARRAY + [ 'child' => self::TYPE_INT ], |
| 42 | 'settings.package' => self::TYPE_BOOL, |
| 43 | 'settings.requiresES6' => self::TYPE_BOOL, |
| 44 | 'settings.rights' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 45 | 'settings.skins' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 46 | 'settings.supportsUrlLoad' => self::TYPE_BOOL, |
| 47 | |
| 48 | 'module' => self::TYPE_ARRAY, |
| 49 | 'module.codexIcons' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 50 | 'module.dependencies' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 51 | 'module.messages' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 52 | 'module.pages' => self::TYPE_ARRAY + [ 'child' => self::TYPE_PAGE_SUFFIX ], |
| 53 | 'module.peers' => self::TYPE_ARRAY + [ 'child' => self::TYPE_STRING ], |
| 54 | 'module.type' => self::TYPE_MODULE_TYPE, |
| 55 | ]; |
| 56 | |
| 57 | /** |
| 58 | * @param mixed $title |
| 59 | * @return bool |
| 60 | */ |
| 61 | public static function isResourcePageSuffix( $title ): bool { |
| 62 | return is_string( $title ) && ( |
| 63 | str_ends_with( $title, '.js' ) || |
| 64 | str_ends_with( $title, '.css' ) || |
| 65 | str_ends_with( $title, '.json' ) || |
| 66 | str_ends_with( $title, '.vue' ) |
| 67 | ); |
| 68 | } |
| 69 | |
| 70 | public static function isModuleType( string $type ): bool { |
| 71 | return $type === '' || $type === 'general' || $type === 'styles'; |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * Check the validity of known properties in a gadget definition array. |
| 76 | * |
| 77 | * @param array $definition |
| 78 | * @param bool $tolerateMissing If true, don't complain about missing keys |
| 79 | * @return Status object with error message if applicable |
| 80 | */ |
| 81 | public function validate( array $definition, $tolerateMissing = false ) { |
| 82 | foreach ( self::$schema as $property => $validation ) { |
| 83 | // Access the property by walking from the root to the specified property |
| 84 | $val = $definition; |
| 85 | foreach ( explode( '.', $property ) as $propName ) { |
| 86 | if ( !array_key_exists( $propName, $val ) ) { |
| 87 | if ( $tolerateMissing ) { |
| 88 | // Skip validation of this property altogether |
| 89 | continue 2; |
| 90 | } |
| 91 | |
| 92 | return Status::newFatal( 'gadgets-validate-notset', $property ); |
| 93 | } |
| 94 | $val = $val[$propName]; |
| 95 | } |
| 96 | |
| 97 | // Validate this property |
| 98 | $isValid = ( $validation['callback'] )( $val ); |
| 99 | if ( !$isValid ) { |
| 100 | return Status::newFatal( |
| 101 | 'gadgets-validate-wrongtype', $property, |
| 102 | $validation['expect'] |
| 103 | ); |
| 104 | } |
| 105 | |
| 106 | // Descend into the array and validate each array item |
| 107 | if ( isset( $validation['child'] ) && is_array( $val ) ) { |
| 108 | foreach ( $val as $key => $item ) { |
| 109 | $isValid = ( $validation['child']['callback'] )( $item ); |
| 110 | if ( !$isValid ) { |
| 111 | return Status::newFatal( |
| 112 | 'gadgets-validate-wrongtype', |
| 113 | "{$property}[{$key}]", |
| 114 | $validation['child']['expect'] |
| 115 | ); |
| 116 | } |
| 117 | } |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | return Status::newGood(); |
| 122 | } |
| 123 | } |