Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
77.03% |
57 / 74 |
|
66.67% |
8 / 12 |
CRAP | |
0.00% |
0 / 1 |
ConfigurationValidator | |
77.03% |
57 / 74 |
|
66.67% |
8 / 12 |
40.91 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setMessageLocalizer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
validateIdentifier | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
validateTitle | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
6.00 | |||
validateRequiredField | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
validateInteger | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
validateFieldIsArray | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
validateArrayMaxSize | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
validateMessages | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
validateTaskMessages | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
validateTopicMessages | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
sortTopics | |
92.86% |
13 / 14 |
|
0.00% |
0 / 1 |
3.00 |
1 | <?php |
2 | |
3 | namespace GrowthExperiments\NewcomerTasks\ConfigurationLoader; |
4 | |
5 | use GrowthExperiments\NewcomerTasks\TaskType\TaskType; |
6 | use GrowthExperiments\NewcomerTasks\Topic\Topic; |
7 | use MediaWiki\Collation\CollationFactory; |
8 | use MediaWiki\Message\Message; |
9 | use MediaWiki\Title\MalformedTitleException; |
10 | use MediaWiki\Title\TitleParser; |
11 | use MessageLocalizer; |
12 | use StatusValue; |
13 | |
14 | /** |
15 | * Helper class for validating task type / topic / etc. configuration. |
16 | */ |
17 | class ConfigurationValidator { |
18 | |
19 | /** @var MessageLocalizer */ |
20 | private $messageLocalizer; |
21 | |
22 | /** @var CollationFactory */ |
23 | private $collationFactory; |
24 | |
25 | /** @var TitleParser */ |
26 | private $titleParser; |
27 | |
28 | /** |
29 | * @param MessageLocalizer $messageLocalizer |
30 | * @param CollationFactory $collationFactory |
31 | * @param TitleParser $titleParser |
32 | */ |
33 | public function __construct( |
34 | MessageLocalizer $messageLocalizer, |
35 | CollationFactory $collationFactory, |
36 | TitleParser $titleParser |
37 | ) { |
38 | $this->messageLocalizer = $messageLocalizer; |
39 | $this->collationFactory = $collationFactory; |
40 | $this->titleParser = $titleParser; |
41 | } |
42 | |
43 | /** |
44 | * Inject the message localizer. |
45 | * @param MessageLocalizer $messageLocalizer |
46 | * @internal To be used by ResourceLoader callbacks only. |
47 | * @note This is an ugly hack. Normal requests use the global RequestContext as a localizer, |
48 | * which is a bit of a kitchen sink, but conceptually can be thought of as a service. |
49 | * ResourceLoader provides the ResourceLoaderContext, which is not global and can only be |
50 | * obtained by code directly invoked by ResourceLoader. The ConfigurationLoader depends |
51 | * on whichever of the two is available, so the localizer cannot be injected in the service |
52 | * wiring file, and a factory would not make sense conceptually (there should never be |
53 | * multiple configuration loaders). So we provide this method so that the ResourceLoader |
54 | * callback can finish the dependency injection. |
55 | */ |
56 | public function setMessageLocalizer( MessageLocalizer $messageLocalizer ): void { |
57 | $this->messageLocalizer = $messageLocalizer; |
58 | } |
59 | |
60 | /** |
61 | * Validate a task or topic ID |
62 | * @param string $id |
63 | * @return StatusValue |
64 | */ |
65 | public function validateIdentifier( $id ) { |
66 | return preg_match( '/^[a-z\d\-]+$/', $id ) |
67 | ? StatusValue::newGood() |
68 | : StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-invalidid', $id ); |
69 | } |
70 | |
71 | /** |
72 | * @param mixed $title Page title. Must be a string (but at the PHP level we need to allow |
73 | * any type, so we can handle errors via status objects). |
74 | * @return StatusValue |
75 | */ |
76 | public function validateTitle( $title ) { |
77 | if ( !is_string( $title ) ) { |
78 | if ( !is_scalar( $title ) ) { |
79 | $title = '[' . gettype( $title ) . ']'; |
80 | } |
81 | return StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-invalidtitle', |
82 | $title ); |
83 | } |
84 | try { |
85 | $this->titleParser->parseTitle( $title ); |
86 | } catch ( MalformedTitleException $e ) { |
87 | return StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-invalidtitle', |
88 | $title ); |
89 | } |
90 | return StatusValue::newGood(); |
91 | } |
92 | |
93 | /** |
94 | * Verify that a required field is present. |
95 | * @param string $field Configuration field name |
96 | * @param array $config Configuration |
97 | * @param string $taskTypeId Task type ID, for better error reporting |
98 | * @return StatusValue |
99 | */ |
100 | public function validateRequiredField( $field, $config, $taskTypeId ) { |
101 | return isset( $config[$field] ) |
102 | ? StatusValue::newGood() |
103 | : StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-missingfield', |
104 | $field, $taskTypeId ); |
105 | } |
106 | |
107 | /** |
108 | * Verify that a field is an integer, and optionally within some bounds. The field doesn't have |
109 | * to exist. |
110 | * @param array $config Configuration |
111 | * @param string $field Configuration field name |
112 | * @param string $taskTypeId Task type ID, for better error reporting |
113 | * @param int|null $min Minimum value |
114 | * @return StatusValue |
115 | */ |
116 | public function validateInteger( |
117 | array $config, string $field, string $taskTypeId, ?int $min = null |
118 | ) { |
119 | if ( !array_key_exists( $field, $config ) ) { |
120 | return StatusValue::newGood(); |
121 | } |
122 | $value = $config[$field]; |
123 | if ( !is_int( $value ) ) { |
124 | return StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-notinteger', |
125 | $field, $taskTypeId ); |
126 | } elseif ( $min !== null && $value < $min ) { |
127 | return StatusValue::newFatal( 'growthexperiments-homepage-suggestededits-config-toosmall', |
128 | $field, $taskTypeId, $min ); |
129 | } |
130 | return StatusValue::newGood(); |
131 | } |
132 | |
133 | /** |
134 | * Verify that a field exists and is a non-associative array. |
135 | * |
136 | * @param string $field Configuration field name |
137 | * @param array $config Configuration |
138 | * @param string $taskTypeId Task type ID, for better error reporting. |
139 | * @return StatusValue |
140 | */ |
141 | public function validateFieldIsArray( string $field, array $config, string $taskTypeId ): StatusValue { |
142 | $status = StatusValue::newGood(); |
143 | $status->merge( $this->validateRequiredField( $field, $config, $taskTypeId ) ); |
144 | if ( $status->isOK() ) { |
145 | if ( !is_array( $config[$field] ) || array_values( $config[$field] ) !== $config[$field] ) { |
146 | $status->fatal( |
147 | 'growthexperiments-homepage-suggestededits-config-fieldarray', $taskTypeId, $field |
148 | ); |
149 | } |
150 | } |
151 | return $status; |
152 | } |
153 | |
154 | /** |
155 | * Verify that an array doesn't exceed an allowed maximum size. |
156 | * |
157 | * @param int $maxSize |
158 | * @param array $config Configuration |
159 | * @param string $taskTypeId Task type ID, for better error reporting. |
160 | * @param string $field Configuration field name, for better error reporting. |
161 | * @return StatusValue |
162 | */ |
163 | public function validateArrayMaxSize( int $maxSize, array $config, string $taskTypeId, string $field ) { |
164 | $status = StatusValue::newGood(); |
165 | if ( count( $config ) > $maxSize ) { |
166 | $status->fatal( 'growthexperiments-homepage-suggestededits-config-arraymaxsize', |
167 | $taskTypeId, $field, Message::numParam( $maxSize ) ); |
168 | } |
169 | return $status; |
170 | } |
171 | |
172 | /** |
173 | * For a given list of messages, verifies that they all exist. |
174 | * @param Message[] $messages |
175 | * @param string $field Field name where the missing message was defined (e.g. ID of the task). |
176 | * @return StatusValue |
177 | */ |
178 | public function validateMessages( array $messages, string $field ) { |
179 | $status = StatusValue::newGood(); |
180 | foreach ( $messages as $msg ) { |
181 | if ( !$msg->exists() ) { |
182 | $status->fatal( 'growthexperiments-homepage-suggestededits-config-missingmessage', |
183 | $msg->getKey(), $field ); |
184 | } |
185 | } |
186 | return $status; |
187 | } |
188 | |
189 | /** |
190 | * Ensure that all messages used by the task type exist. |
191 | * @param TaskType $taskType |
192 | * @return StatusValue |
193 | */ |
194 | public function validateTaskMessages( TaskType $taskType ) { |
195 | return $this->validateMessages( [ |
196 | $taskType->getName( $this->messageLocalizer ), |
197 | $taskType->getDescription( $this->messageLocalizer ), |
198 | $taskType->getShortDescription( $this->messageLocalizer ), |
199 | $taskType->getTimeEstimate( $this->messageLocalizer ) |
200 | ], $taskType->getId() ); |
201 | } |
202 | |
203 | /** |
204 | * Ensure that all messages used by the topic exist. |
205 | * @param Topic $topic |
206 | * @return StatusValue |
207 | */ |
208 | public function validateTopicMessages( Topic $topic ) { |
209 | $messages = [ $topic->getName( $this->messageLocalizer ) ]; |
210 | if ( $topic->getGroupId() ) { |
211 | $messages[] = $topic->getGroupName( $this->messageLocalizer ); |
212 | } |
213 | return $this->validateMessages( $messages, $topic->getId() ); |
214 | } |
215 | |
216 | /** |
217 | * Sorts topics in-place, based on the group configuration and alphabetically within that. |
218 | * @param Topic[] &$topics |
219 | * @param string[] $groups |
220 | */ |
221 | public function sortTopics( array &$topics, $groups ) { |
222 | if ( !$topics ) { |
223 | return; |
224 | } |
225 | |
226 | $collation = $this->collationFactory->getCategoryCollation(); |
227 | |
228 | usort( $topics, function ( Topic $left, Topic $right ) use ( $groups, $collation ) { |
229 | $leftGroup = $left->getGroupId(); |
230 | $rightGroup = $right->getGroupId(); |
231 | if ( $leftGroup !== $rightGroup ) { |
232 | return array_search( $leftGroup, $groups, true ) - array_search( $rightGroup, $groups, true ); |
233 | } |
234 | |
235 | $leftSortKey = $collation->getSortKey( |
236 | $left->getName( $this->messageLocalizer )->text() ); |
237 | $rightSortKey = $collation->getSortKey( |
238 | $right->getName( $this->messageLocalizer )->text() ); |
239 | return strcmp( $leftSortKey, $rightSortKey ); |
240 | } ); |
241 | } |
242 | |
243 | } |