Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GadgetDefinitionValidator
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 3
342
0.00% covered (danger)
0.00%
0 / 1
 isResourcePageSuffix
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 isModuleType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
12
 validate
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2
3namespace MediaWiki\Extension\Gadgets\Content;
4
5use MediaWiki\Status\Status;
6
7/**
8 * Validate the content of a gadget definition page.
9 *
10 * @see MediaWikiGadgetsJsonRepo
11 * @see GadgetDefinitionContent
12 * @internal
13 */
14class 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}