Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.83% |
45 / 46 |
|
80.00% |
4 / 5 |
CRAP | |
0.00% |
0 / 1 |
Validator | |
97.83% |
45 / 46 |
|
80.00% |
4 / 5 |
10 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
validate | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
1 | |||
makeSchema | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
doValidate | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
4 | |||
arrayToObject | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\MediaUploader\Campaign; |
4 | |
5 | use BagOStuff; |
6 | use MediaWiki\Extension\MediaUploader\Config\RawConfig; |
7 | use MediaWiki\Status\Status; |
8 | use stdClass; |
9 | use Symfony\Component\Yaml\Yaml; |
10 | |
11 | /** |
12 | * Campaign schema validator. |
13 | */ |
14 | class 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 | } |