Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.83% covered (success)
97.83%
45 / 46
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Validator
97.83% covered (success)
97.83%
45 / 46
80.00% covered (warning)
80.00%
4 / 5
10
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 validate
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 makeSchema
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 doValidate
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
4
 arrayToObject
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
1<?php
2
3namespace MediaWiki\Extension\MediaUploader\Campaign;
4
5use BagOStuff;
6use MediaWiki\Extension\MediaUploader\Config\RawConfig;
7use Status;
8use stdClass;
9use Symfony\Component\Yaml\Yaml;
10
11/**
12 * Campaign schema validator.
13 */
14class Validator {
15
16    /** @var RawConfig */
17    private $rawConfig;
18
19    /** @var BagOStuff */
20    private $localServerCache;
21
22    /**
23     * @param RawConfig $rawConfig
24     * @param BagOStuff $localServerCache
25     */
26    public function __construct(
27        RawConfig $rawConfig,
28        BagOStuff $localServerCache
29    ) {
30        $this->rawConfig = $rawConfig;
31        $this->localServerCache = $localServerCache;
32    }
33
34    /**
35     * Validates an object against campaign JSON Schema.
36     *
37     * @param array $object The campaign to validate. Must be in an associative array form.
38     *
39     * @return Status with validation errors (if any)
40     */
41    public function validate( array $object ): Status {
42        $cacheKey = $this->localServerCache->makeKey(
43            'mediauploader',
44            'campaign-schema'
45        );
46        $schema = $this->localServerCache->getWithSetCallback(
47            $cacheKey,
48            $this->localServerCache::TTL_MINUTE * 5,
49            function (): stdClass {
50                return $this->makeSchema();
51            }
52        );
53
54        return $this->doValidate( $object, $schema );
55    }
56
57    /**
58     * Reads the schema from the YAML file and fills in the required gaps.
59     *
60     * @return stdClass
61     */
62    private function makeSchema(): stdClass {
63        $schema = Yaml::parseFile(
64            MU_SCHEMA_DIR . 'campaign.yaml',
65            Yaml::PARSE_OBJECT_FOR_MAP | Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE
66        );
67        $schema->definitions->licenseName->enum = array_keys(
68            $this->rawConfig->getSetting( 'licenses' )
69        );
70
71        return $schema;
72    }
73
74    /**
75     * Does the actual validation work.
76     *
77     * @param array $array
78     * @param stdClass $schema
79     *
80     * @return Status
81     */
82    private function doValidate( array $array, stdClass $schema ): Status {
83        $validator = new \JsonSchema\Validator();
84        $objectStatus = self::arrayToObject( $array );
85
86        if ( !$objectStatus->isGood() ) {
87            return $objectStatus;
88        }
89
90        $object = $objectStatus->getValue();
91        $validator->validate( $object, $schema );
92
93        if ( $validator->isValid() ) {
94            return Status::newGood();
95        }
96
97        $status = new Status();
98        foreach ( $validator->getErrors() as $error ) {
99            $status->fatal(
100                'mediauploader-schema-validation-error',
101                $error['property'],
102                $error['message'],
103                // Pass the inner error with all the details
104                $error
105            );
106        }
107
108        return $status;
109    }
110
111    /**
112     * Converts an array to PHP object, while restricting the array's
113     * maximum depth to 8.
114     *
115     * @param array $array
116     *
117     * @return Status
118     */
119    private static function arrayToObject( array $array ): Status {
120        // Encode with max depth of 8
121        // This should be enough for campaign configs.
122        $json = json_encode( $array, 0, 8 );
123
124        $code = json_last_error();
125        if ( $code === JSON_ERROR_NONE ) {
126            return Status::newGood( (object)json_decode( $json ) );
127        } elseif ( $code === JSON_ERROR_DEPTH ) {
128            return Status::newFatal( 'json-error-depth' );
129        } else {
130            return Status::newFatal( 'json-error-unknown', $code );
131        }
132    }
133}