Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.45% |
177 / 220 |
|
45.45% |
5 / 11 |
CRAP | |
0.00% |
0 / 1 |
EventFactory | |
80.45% |
177 / 220 |
|
45.45% |
5 / 11 |
125.60 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
newEvent | |
90.24% |
74 / 82 |
|
0.00% |
0 / 1 |
12.13 | |||
validatePage | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
8 | |||
validateWikis | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
validateTrackingTool | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
8 | |||
validateTimezone | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
7.04 | |||
validateLocalDates | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
9 | |||
validateMeetingInfo | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
182 | |||
isValidURL | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
10.14 | |||
validateLocation | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
validateParticipantQuestions | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | declare( strict_types=1 ); |
4 | |
5 | namespace MediaWiki\Extension\CampaignEvents\Event; |
6 | |
7 | use DateTime; |
8 | use DateTimeZone; |
9 | use Exception; |
10 | use InvalidArgumentException; |
11 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory; |
12 | use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFormatter; |
13 | use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsPage; |
14 | use MediaWiki\Extension\CampaignEvents\MWEntity\InvalidTitleStringException; |
15 | use MediaWiki\Extension\CampaignEvents\MWEntity\PageNotFoundException; |
16 | use MediaWiki\Extension\CampaignEvents\MWEntity\UnexpectedInterwikiException; |
17 | use MediaWiki\Extension\CampaignEvents\MWEntity\UnexpectedSectionAnchorException; |
18 | use MediaWiki\Extension\CampaignEvents\MWEntity\UnexpectedVirtualNamespaceException; |
19 | use MediaWiki\Extension\CampaignEvents\MWEntity\WikiLookup; |
20 | use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry; |
21 | use MediaWiki\Extension\CampaignEvents\Questions\UnknownQuestionException; |
22 | use MediaWiki\Extension\CampaignEvents\TrackingTool\ToolNotFoundException; |
23 | use MediaWiki\Extension\CampaignEvents\TrackingTool\TrackingToolAssociation; |
24 | use MediaWiki\Extension\CampaignEvents\TrackingTool\TrackingToolRegistry; |
25 | use MediaWiki\Message\Message; |
26 | use MediaWiki\Utils\MWTimestamp; |
27 | use StatusValue; |
28 | use Wikimedia\Message\ListType; |
29 | use Wikimedia\Message\MessageValue; |
30 | use Wikimedia\RequestTimeout\TimeoutException; |
31 | |
32 | class EventFactory { |
33 | public const SERVICE_NAME = 'CampaignEventsEventFactory'; |
34 | public const VALIDATE_ALL = 0; |
35 | public const VALIDATE_SKIP_DATES_PAST = 1 << 0; |
36 | |
37 | public const MAX_WIKIS = 100; |
38 | |
39 | private CampaignsPageFactory $campaignsPageFactory; |
40 | private CampaignsPageFormatter $campaignsPageFormatter; |
41 | private TrackingToolRegistry $trackingToolRegistry; |
42 | private EventQuestionsRegistry $eventQuestionsRegistry; |
43 | private WikiLookup $wikiLookup; |
44 | |
45 | public function __construct( |
46 | CampaignsPageFactory $campaignsPageFactory, |
47 | CampaignsPageFormatter $campaignsPageFormatter, |
48 | TrackingToolRegistry $trackingToolRegistry, |
49 | EventQuestionsRegistry $eventQuestionsRegistry, |
50 | WikiLookup $wikiLookup |
51 | ) { |
52 | $this->campaignsPageFactory = $campaignsPageFactory; |
53 | $this->campaignsPageFormatter = $campaignsPageFormatter; |
54 | $this->trackingToolRegistry = $trackingToolRegistry; |
55 | $this->eventQuestionsRegistry = $eventQuestionsRegistry; |
56 | $this->wikiLookup = $wikiLookup; |
57 | } |
58 | |
59 | /** |
60 | * Creates a new event registration entity, making sure that the given data is valid and formatted as expected. |
61 | * |
62 | * @param int|null $id |
63 | * @param string $pageTitleStr |
64 | * @param string|null $chatURL |
65 | * @param string[]|true $wikis List of wiki IDs, or {@see EventRegistration::ALL_WIKIS} |
66 | * @param string|null $trackingToolUserID User identifier of a tracking tool |
67 | * @param string|null $trackingToolEventID |
68 | * @param string $status |
69 | * @param string $timezone Can be in any format accepted by DateTimeZone |
70 | * @param string $startLocalTimestamp In the TS_MW format |
71 | * @param string $endLocalTimestamp In the TS_MW format |
72 | * @param string $type |
73 | * @param int $meetingType |
74 | * @param string|null $meetingURL |
75 | * @param string|null $meetingCountry |
76 | * @param string|null $meetingAddress |
77 | * @param string[] $participantQuestionNames |
78 | * @param string|null $creationTimestamp In the TS_MW format |
79 | * @param string|null $lastEditTimestamp In the TS_MW format |
80 | * @param string|null $deletionTimestamp In the TS_MW format |
81 | * @param int $validationFlags |
82 | * @return EventRegistration |
83 | * @throws InvalidEventDataException |
84 | */ |
85 | public function newEvent( |
86 | ?int $id, |
87 | string $pageTitleStr, |
88 | ?string $chatURL, |
89 | $wikis, |
90 | ?string $trackingToolUserID, |
91 | ?string $trackingToolEventID, |
92 | string $status, |
93 | string $timezone, |
94 | string $startLocalTimestamp, |
95 | string $endLocalTimestamp, |
96 | string $type, |
97 | int $meetingType, |
98 | ?string $meetingURL, |
99 | ?string $meetingCountry, |
100 | ?string $meetingAddress, |
101 | array $participantQuestionNames, |
102 | ?string $creationTimestamp, |
103 | ?string $lastEditTimestamp, |
104 | ?string $deletionTimestamp, |
105 | int $validationFlags = self::VALIDATE_ALL |
106 | ): EventRegistration { |
107 | $res = StatusValue::newGood(); |
108 | |
109 | if ( $id !== null && $id <= 0 ) { |
110 | $res->error( 'campaignevents-error-invalid-id' ); |
111 | } |
112 | |
113 | $pageStatus = $this->validatePage( $pageTitleStr ); |
114 | $res->merge( $pageStatus ); |
115 | $campaignsPage = $pageStatus->getValue(); |
116 | |
117 | if ( $chatURL !== null ) { |
118 | $chatURL = trim( $chatURL ); |
119 | if ( !$this->isValidURL( $chatURL ) ) { |
120 | $res->error( 'campaignevents-error-invalid-chat-url' ); |
121 | } |
122 | } |
123 | |
124 | $wikisStatus = $this->validateWikis( $wikis ); |
125 | $res->merge( $wikisStatus ); |
126 | $wikis = $wikisStatus->getValue(); |
127 | |
128 | $trackingToolStatus = $this->validateTrackingTool( $trackingToolUserID, $trackingToolEventID ); |
129 | $res->merge( $trackingToolStatus ); |
130 | $trackingToolDBID = $trackingToolStatus->getValue(); |
131 | if ( $trackingToolDBID !== null ) { |
132 | $trackingTools = [ |
133 | new TrackingToolAssociation( |
134 | $trackingToolDBID, |
135 | $trackingToolEventID, |
136 | TrackingToolAssociation::SYNC_STATUS_UNKNOWN, |
137 | null |
138 | ) |
139 | ]; |
140 | } else { |
141 | $trackingTools = []; |
142 | } |
143 | |
144 | if ( !in_array( $status, EventRegistration::VALID_STATUSES, true ) ) { |
145 | $res->error( 'campaignevents-error-invalid-status' ); |
146 | } |
147 | |
148 | $timezoneStatus = $this->validateTimezone( $timezone ); |
149 | $res->merge( $timezoneStatus ); |
150 | $timezoneObj = $timezoneStatus->isGood() ? $timezoneStatus->getValue() : null; |
151 | if ( $timezoneObj ) { |
152 | $datesStatus = $this->validateLocalDates( |
153 | $validationFlags, |
154 | $timezoneObj, |
155 | $startLocalTimestamp, |
156 | $endLocalTimestamp |
157 | ); |
158 | $res->merge( $datesStatus ); |
159 | } |
160 | |
161 | if ( !in_array( $type, EventRegistration::VALID_TYPES, true ) ) { |
162 | $res->error( 'campaignevents-error-invalid-type' ); |
163 | } |
164 | |
165 | $res->merge( $this->validateMeetingInfo( $meetingType, $meetingURL, $meetingCountry, $meetingAddress ) ); |
166 | |
167 | $questionsStatus = $this->validateParticipantQuestions( $participantQuestionNames ); |
168 | $res->merge( $questionsStatus ); |
169 | $questionIDs = $questionsStatus->getValue(); |
170 | |
171 | $creationTSUnix = wfTimestampOrNull( TS_UNIX, $creationTimestamp ); |
172 | $lastEditTSUnix = wfTimestampOrNull( TS_UNIX, $lastEditTimestamp ); |
173 | $deletionTSUnix = wfTimestampOrNull( TS_UNIX, $deletionTimestamp ); |
174 | // Creation, last edit, and deletion timestamp don't need user-facing validation since it's not the |
175 | // user setting them. |
176 | $invalidTimestamps = array_filter( |
177 | [ 'creation' => $creationTSUnix, 'lastedit' => $lastEditTSUnix, 'deletion' => $deletionTSUnix ], |
178 | static function ( $ts ): bool { |
179 | return $ts === false; |
180 | } |
181 | ); |
182 | if ( $invalidTimestamps ) { |
183 | throw new InvalidArgumentException( |
184 | "Invalid timestamps: " . implode( ', ', array_keys( $invalidTimestamps ) ) |
185 | ); |
186 | } |
187 | |
188 | if ( !$res->isGood() ) { |
189 | throw new InvalidEventDataException( $res ); |
190 | } |
191 | |
192 | /** @var ICampaignsPage $campaignsPage */ |
193 | '@phan-var ICampaignsPage $campaignsPage'; |
194 | |
195 | return new EventRegistration( |
196 | $id, |
197 | $this->campaignsPageFormatter->getText( $campaignsPage ), |
198 | $campaignsPage, |
199 | $chatURL, |
200 | $wikis, |
201 | [], |
202 | $trackingTools, |
203 | $status, |
204 | $timezoneObj, |
205 | $startLocalTimestamp, |
206 | $endLocalTimestamp, |
207 | $type, |
208 | $meetingType, |
209 | $meetingURL, |
210 | $meetingCountry, |
211 | $meetingAddress, |
212 | $questionIDs, |
213 | $creationTSUnix, |
214 | $lastEditTSUnix, |
215 | $deletionTSUnix |
216 | ); |
217 | } |
218 | |
219 | /** |
220 | * Validates the page title provided as a string. |
221 | * |
222 | * @param string $pageTitleStr |
223 | * @return StatusValue Fatal if invalid, good otherwise and with an ICampaignsPage as value. |
224 | */ |
225 | private function validatePage( string $pageTitleStr ): StatusValue { |
226 | $pageTitleStr = trim( $pageTitleStr ); |
227 | if ( $pageTitleStr === '' ) { |
228 | return StatusValue::newFatal( 'campaignevents-error-empty-title' ); |
229 | } |
230 | |
231 | try { |
232 | $campaignsPage = $this->campaignsPageFactory->newLocalExistingPageFromString( $pageTitleStr ); |
233 | } catch ( InvalidTitleStringException $e ) { |
234 | return StatusValue::newFatal( |
235 | 'campaignevents-error-invalid-title', |
236 | new MessageValue( $e->getErrorMsgKey(), $e->getErrorMsgParams() ) |
237 | ); |
238 | } catch ( UnexpectedInterwikiException $_ ) { |
239 | return StatusValue::newFatal( 'campaignevents-error-invalid-title-interwiki' ); |
240 | } catch ( UnexpectedVirtualNamespaceException $_ ) { |
241 | return StatusValue::newFatal( 'campaignevents-error-page-not-event-namespace' ); |
242 | } catch ( UnexpectedSectionAnchorException $_ ) { |
243 | return StatusValue::newFatal( 'campaignevents-error-page-with-section' ); |
244 | } catch ( PageNotFoundException $_ ) { |
245 | return StatusValue::newFatal( 'campaignevents-error-page-not-found' ); |
246 | } |
247 | |
248 | if ( $campaignsPage->getNamespace() !== NS_EVENT ) { |
249 | return StatusValue::newFatal( 'campaignevents-error-page-not-event-namespace' ); |
250 | } |
251 | |
252 | return StatusValue::newGood( $campaignsPage ); |
253 | } |
254 | |
255 | /** |
256 | * @param string[]|true $wikis |
257 | * @return StatusValue Having a canonicalized list of wiki IDs as value. |
258 | */ |
259 | private function validateWikis( $wikis ): StatusValue { |
260 | if ( $wikis === EventRegistration::ALL_WIKIS ) { |
261 | return StatusValue::newGood( $wikis ); |
262 | } |
263 | |
264 | $wikis = array_unique( $wikis ); |
265 | $ret = StatusValue::newGood( $wikis ); |
266 | if ( count( $wikis ) > self::MAX_WIKIS ) { |
267 | $ret->error( 'campaignevents-error-too-many-wikis', Message::numParam( self::MAX_WIKIS ) ); |
268 | } |
269 | |
270 | $invalidWikis = array_diff( $wikis, $this->wikiLookup->getAllWikis() ); |
271 | if ( $invalidWikis ) { |
272 | $ret->error( |
273 | 'campaignevents-error-invalid-wikis', |
274 | Message::listParam( $invalidWikis, ListType::COMMA ) |
275 | ); |
276 | } |
277 | |
278 | return $ret; |
279 | } |
280 | |
281 | /** |
282 | * @param string|null $trackingToolUserID |
283 | * @param string|null $trackingToolEventID |
284 | * @return StatusValue If good, has the tracking tool DB ID as value, or null if no tool was specified. |
285 | */ |
286 | private function validateTrackingTool( ?string $trackingToolUserID, ?string $trackingToolEventID ): StatusValue { |
287 | if ( $trackingToolUserID === null || $trackingToolEventID === null ) { |
288 | if ( $trackingToolUserID !== null && $trackingToolEventID === null ) { |
289 | return StatusValue::newFatal( 'campaignevents-error-trackingtool-without-eventid' ); |
290 | } |
291 | if ( $trackingToolUserID === null && $trackingToolEventID !== null ) { |
292 | return StatusValue::newFatal( 'campaignevents-error-trackingtool-eventid-without-toolid' ); |
293 | } |
294 | return StatusValue::newGood( null ); |
295 | } |
296 | |
297 | try { |
298 | return StatusValue::newGood( |
299 | $this->trackingToolRegistry->newFromUserIdentifier( $trackingToolUserID )->getDBID() |
300 | ); |
301 | } catch ( ToolNotFoundException $_ ) { |
302 | return StatusValue::newFatal( 'campaignevents-error-invalid-trackingtool' ); |
303 | } |
304 | } |
305 | |
306 | /** |
307 | * @param string $timezone |
308 | * @return StatusValue If good, has the corresponding DateTimeZone object as value. |
309 | */ |
310 | private function validateTimezone( string $timezone ): StatusValue { |
311 | if ( preg_match( '/^[+-]/', $timezone ) ) { |
312 | $matches = []; |
313 | if ( !preg_match( '/^[+-](\d\d):(\d\d)$/', $timezone, $matches ) ) { |
314 | // Work around bug in PHP: strings starting with + and - do not throw an exception in PHP < 8, |
315 | // see https://3v4l.org/SE0oA. This also rejects offsets where the hours or the minutes have more |
316 | // than 3 digits, which PHP accepts but then does not handle properly; the exact meaning of "not handle |
317 | // properly" depends on the PHP version, see https://github.com/php/php-src/issues/9763#issue-1411450292 |
318 | return StatusValue::newFatal( 'campaignevents-error-invalid-timezone' ); |
319 | } |
320 | // Work around another PHP bug: if the hours are < 100 but hours + 60 * miutes >= 100*60, it will truncate |
321 | // the input and add a null byte that makes it unusable, see https://github.com/php/php-src/issues/9763 |
322 | if ( $matches[1] === '99' && (int)$matches[2] >= 60 ) { |
323 | return StatusValue::newFatal( 'campaignevents-error-invalid-timezone' ); |
324 | } |
325 | } |
326 | try { |
327 | return StatusValue::newGood( new DateTimeZone( $timezone ) ); |
328 | } catch ( TimeoutException $e ) { |
329 | throw $e; |
330 | } catch ( Exception $e ) { |
331 | // PHP throws a generic Exception, but we don't want to catch excimer timeouts. |
332 | // Again, thanks PHP for making error handling so convoluted here. |
333 | // See https://github.com/php/php-src/issues/9784 |
334 | return StatusValue::newFatal( 'campaignevents-error-invalid-timezone' ); |
335 | } |
336 | } |
337 | |
338 | /** |
339 | * @param int $validationFlags |
340 | * @param DateTimeZone $timezone |
341 | * @param string $start |
342 | * @param string $end |
343 | * @return StatusValue |
344 | */ |
345 | private function validateLocalDates( |
346 | int $validationFlags, |
347 | DateTimeZone $timezone, |
348 | string $start, |
349 | string $end |
350 | ): StatusValue { |
351 | $res = StatusValue::newGood(); |
352 | |
353 | $startTSUnix = null; |
354 | $endTSUnix = null; |
355 | $startAndEndValid = true; |
356 | if ( $start === '' ) { |
357 | $startAndEndValid = false; |
358 | $res->error( 'campaignevents-error-empty-start' ); |
359 | } elseif ( MWTimestamp::convert( TS_MW, $start ) !== $start ) { |
360 | // This accounts for both the timestamp being invalid and it not being TS_MW. |
361 | $startAndEndValid = false; |
362 | $res->error( 'campaignevents-error-invalid-start' ); |
363 | } else { |
364 | $startTSUnix = ( new DateTime( $start, $timezone ) )->getTimestamp(); |
365 | if ( |
366 | !( $validationFlags & self::VALIDATE_SKIP_DATES_PAST ) && $startTSUnix < MWTimestamp::time() |
367 | ) { |
368 | $res->error( 'campaignevents-error-start-past' ); |
369 | } |
370 | } |
371 | |
372 | if ( $end === '' ) { |
373 | $startAndEndValid = false; |
374 | $res->error( 'campaignevents-error-empty-end' ); |
375 | } elseif ( MWTimestamp::convert( TS_MW, $end ) !== $end ) { |
376 | // This accounts for both the timestamp being invalid and it not being TS_MW. |
377 | $startAndEndValid = false; |
378 | $res->error( 'campaignevents-error-invalid-end' ); |
379 | } else { |
380 | $endTSUnix = ( new DateTime( $end, $timezone ) )->getTimestamp(); |
381 | } |
382 | |
383 | if ( $startAndEndValid && $startTSUnix > $endTSUnix ) { |
384 | $res->error( 'campaignevents-error-start-after-end' ); |
385 | } |
386 | |
387 | return $res; |
388 | } |
389 | |
390 | /** |
391 | * @param int $meetingType |
392 | * @param string|null &$meetingURL |
393 | * @param string|null &$meetingCountry |
394 | * @param string|null &$meetingAddress |
395 | * @return StatusValue |
396 | */ |
397 | private function validateMeetingInfo( |
398 | int $meetingType, |
399 | ?string &$meetingURL, |
400 | ?string &$meetingCountry, |
401 | ?string &$meetingAddress |
402 | ): StatusValue { |
403 | $res = StatusValue::newGood(); |
404 | |
405 | if ( !in_array( $meetingType, EventRegistration::VALID_MEETING_TYPES, true ) ) { |
406 | $res->error( 'campaignevents-error-no-meeting-type' ); |
407 | // Don't bother checking the rest. |
408 | return $res; |
409 | } |
410 | |
411 | if ( $meetingType & EventRegistration::MEETING_TYPE_ONLINE ) { |
412 | if ( $meetingURL !== null ) { |
413 | $meetingURL = trim( $meetingURL ); |
414 | if ( !$this->isValidURL( $meetingURL ) ) { |
415 | $res->error( 'campaignevents-error-invalid-meeting-url' ); |
416 | } |
417 | } |
418 | } elseif ( $meetingURL !== null ) { |
419 | $res->error( 'campaignevents-error-meeting-url-not-online' ); |
420 | } |
421 | |
422 | if ( $meetingType & EventRegistration::MEETING_TYPE_IN_PERSON ) { |
423 | if ( $meetingCountry !== null ) { |
424 | $meetingCountry = trim( $meetingCountry ); |
425 | } |
426 | if ( $meetingAddress !== null ) { |
427 | $meetingAddress = trim( $meetingAddress ); |
428 | } |
429 | if ( $meetingCountry !== null && $meetingAddress !== null ) { |
430 | $res->merge( $this->validateLocation( $meetingCountry, $meetingAddress ) ); |
431 | } |
432 | } elseif ( $meetingCountry !== null || $meetingAddress !== null ) { |
433 | $res->error( 'campaignevents-error-countryoraddress-not-in-person' ); |
434 | } |
435 | return $res; |
436 | } |
437 | |
438 | /** |
439 | * @param string $data |
440 | * @return bool |
441 | */ |
442 | private function isValidURL( string $data ): bool { |
443 | // TODO There's a lot of space for improvement here, e.g., expand the list of allowed protocols, and |
444 | // possibly avoid having to do all the normalization and checks ourselves. |
445 | $allowedSchemes = [ 'http', 'https' ]; |
446 | |
447 | // Add the HTTPS protocol explicitly, since FILTER_VALIDATE_URL wants a scheme. |
448 | $urlToCheck = preg_match( '/^\/\/.*/', $data ) ? "https:$data" : $data; |
449 | $urlParts = parse_url( $urlToCheck ); |
450 | |
451 | // Validate scheme, host presence, and allowed schemes |
452 | if ( |
453 | $urlParts === false || !isset( $urlParts[ 'scheme' ] ) || |
454 | !isset( $urlParts[ 'host' ] ) || |
455 | !in_array( strtolower( $urlParts[ 'scheme' ] ), $allowedSchemes, true ) |
456 | ) { |
457 | return false; |
458 | } |
459 | |
460 | // Convert URL host from IDN to ASCII (Punycode) |
461 | $hostASCII = idn_to_ascii( $urlParts[ 'host' ], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46 ); |
462 | if ( $hostASCII === false ) { |
463 | return false; |
464 | } |
465 | |
466 | // Rebuild the URL with the ASCII host for validation |
467 | $urlToCheckASCII = $urlParts[ 'scheme' ] . "://" . $hostASCII; |
468 | if ( isset( $urlParts[ 'path' ] ) ) { |
469 | $urlToCheckASCII .= '/' . urlencode( $urlParts[ 'path' ] ); |
470 | } |
471 | if ( isset( $urlParts[ 'query' ] ) ) { |
472 | $urlToCheckASCII .= '?' . urlencode( $urlParts[ 'query' ] ); |
473 | } |
474 | if ( isset( $urlParts[ 'fragment' ] ) ) { |
475 | $urlToCheckASCII .= '#' . urlencode( $urlParts[ 'fragment' ] ); |
476 | } |
477 | |
478 | return filter_var( $urlToCheckASCII, FILTER_VALIDATE_URL ) !== false; |
479 | } |
480 | |
481 | /** |
482 | * @param string $country |
483 | * @param string $address |
484 | * @return StatusValue |
485 | */ |
486 | private function validateLocation( string $country, string $address ): StatusValue { |
487 | $res = StatusValue::newGood(); |
488 | if ( $country === '' ) { |
489 | $res->error( 'campaignevents-error-invalid-country' ); |
490 | } |
491 | if ( $address === '' ) { |
492 | $res->error( 'campaignevents-error-invalid-address' ); |
493 | } |
494 | return $res; |
495 | } |
496 | |
497 | /** |
498 | * @param string[] $questionNames |
499 | * @return StatusValue Whose value is an array of the corresponding question DB IDs. |
500 | */ |
501 | private function validateParticipantQuestions( array $questionNames ): StatusValue { |
502 | $questionIDs = []; |
503 | $invalidNames = []; |
504 | foreach ( $questionNames as $name ) { |
505 | try { |
506 | $questionIDs[] = $this->eventQuestionsRegistry->nameToDBID( $name ); |
507 | } catch ( UnknownQuestionException $_ ) { |
508 | $invalidNames[] = $name; |
509 | } |
510 | } |
511 | $ret = StatusValue::newGood( $questionIDs ); |
512 | if ( $invalidNames ) { |
513 | $ret->fatal( 'campaignevents-error-invalid-question names', Message::listParam( $invalidNames ) ); |
514 | } |
515 | return $ret; |
516 | } |
517 | |
518 | } |