Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 895 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
CreatePage | |
0.00% |
0 / 895 |
|
0.00% |
0 / 14 |
19182 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 384 |
|
0.00% |
0 / 1 |
2070 | |||
processInputDuringElection | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
processInput | |
0.00% |
0 / 236 |
|
0.00% |
0 / 1 |
600 | |||
recordElectionToNamespace | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
6 | |||
logAdminChanges | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
getFormDataFromElection | |
0.00% |
0 / 66 |
|
0.00% |
0 / 1 |
342 | |||
insertEntity | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
savePropertiesAndMessages | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
56 | |||
processFormItems | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
110 | |||
unprocessFormData | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
210 | |||
checkIfInElectionAdminUserGroup | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
checkElectionEndDate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
checkRequired | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Pages; |
4 | |
5 | use DateTime; |
6 | use DateTimeZone; |
7 | use HTMLForm; |
8 | use LanguageCode; |
9 | use MediaWiki\Extension\SecurePoll\Context; |
10 | use MediaWiki\Extension\SecurePoll\Crypt\Crypt; |
11 | use MediaWiki\Extension\SecurePoll\Entities\Entity; |
12 | use MediaWiki\Extension\SecurePoll\Exceptions\InvalidDataException; |
13 | use MediaWiki\Extension\SecurePoll\SecurePollContentHandler; |
14 | use MediaWiki\Extension\SecurePoll\SpecialSecurePoll; |
15 | use MediaWiki\Extension\SecurePoll\Store\FormStore; |
16 | use MediaWiki\Extension\SecurePoll\Talliers\Tallier; |
17 | use MediaWiki\Languages\LanguageNameUtils; |
18 | use MediaWiki\Linker\Linker; |
19 | use MediaWiki\Page\WikiPageFactory; |
20 | use MediaWiki\SpecialPage\SpecialPage; |
21 | use MediaWiki\Status\Status; |
22 | use MediaWiki\User\UserFactory; |
23 | use MediaWiki\User\UserGroupManager; |
24 | use MediaWiki\Utils\MWTimestamp; |
25 | use MediaWiki\WikiMap\WikiMap; |
26 | use Message; |
27 | use PermissionsError; |
28 | use Wikimedia\Rdbms\IDatabase; |
29 | use Wikimedia\Rdbms\ILoadBalancer; |
30 | use Wikimedia\Rdbms\LBFactory; |
31 | |
32 | /** |
33 | * Special:SecurePoll subpage for creating or editing a poll |
34 | */ |
35 | class CreatePage extends ActionPage { |
36 | /** @var LBFactory */ |
37 | private $lbFactory; |
38 | |
39 | /** @var UserGroupManager */ |
40 | private $userGroupManager; |
41 | |
42 | /** @var LanguageNameUtils */ |
43 | private $languageNameUtils; |
44 | |
45 | /** @var WikiPageFactory */ |
46 | private $wikiPageFactory; |
47 | |
48 | /** @var UserFactory */ |
49 | private $userFactory; |
50 | |
51 | /** |
52 | * @param SpecialSecurePoll $specialPage |
53 | * @param LBFactory $lbFactory |
54 | * @param UserGroupManager $userGroupManager |
55 | * @param LanguageNameUtils $languageNameUtils |
56 | * @param WikiPageFactory $wikiPageFactory |
57 | * @param UserFactory $userFactory |
58 | */ |
59 | public function __construct( |
60 | SpecialSecurePoll $specialPage, |
61 | LBFactory $lbFactory, |
62 | UserGroupManager $userGroupManager, |
63 | LanguageNameUtils $languageNameUtils, |
64 | WikiPageFactory $wikiPageFactory, |
65 | UserFactory $userFactory |
66 | ) { |
67 | parent::__construct( $specialPage ); |
68 | $this->lbFactory = $lbFactory; |
69 | $this->userGroupManager = $userGroupManager; |
70 | $this->languageNameUtils = $languageNameUtils; |
71 | $this->wikiPageFactory = $wikiPageFactory; |
72 | $this->userFactory = $userFactory; |
73 | } |
74 | |
75 | /** |
76 | * Execute the subpage. |
77 | * @param array $params Array of subpage parameters. |
78 | * @throws InvalidDataException |
79 | * @throws PermissionsError |
80 | */ |
81 | public function execute( $params ) { |
82 | $out = $this->specialPage->getOutput(); |
83 | |
84 | if ( $params ) { |
85 | $out->setPageTitleMsg( $this->msg( 'securepoll-edit-title' ) ); |
86 | $electionId = intval( $params[0] ); |
87 | $this->election = $this->context->getElection( $electionId ); |
88 | if ( !$this->election ) { |
89 | $out->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
90 | |
91 | return; |
92 | } |
93 | if ( !$this->election->isAdmin( $this->specialPage->getUser() ) ) { |
94 | $out->addWikiMsg( 'securepoll-need-admin' ); |
95 | |
96 | return; |
97 | } |
98 | if ( $this->election->isFinished() ) { |
99 | $out->addWikiMsg( 'securepoll-finished-no-edit' ); |
100 | return; |
101 | } |
102 | |
103 | $jumpUrl = $this->election->getProperty( 'jump-url' ); |
104 | if ( $jumpUrl ) { |
105 | $jumpId = $this->election->getProperty( 'jump-id' ); |
106 | if ( !$jumpId ) { |
107 | throw new InvalidDataException( 'Configuration error: no jump-id' ); |
108 | } |
109 | $jumpUrl .= "/edit/$jumpId"; |
110 | if ( count( $params ) > 1 ) { |
111 | $jumpUrl .= '/' . implode( '/', array_slice( $params, 1 ) ); |
112 | } |
113 | |
114 | $wiki = $this->election->getProperty( 'main-wiki' ); |
115 | if ( $wiki ) { |
116 | $wiki = WikiMap::getWikiName( $wiki ); |
117 | } else { |
118 | $wiki = $this->msg( 'securepoll-edit-redirect-otherwiki' )->text(); |
119 | } |
120 | |
121 | $out->addWikiMsg( |
122 | 'securepoll-edit-redirect', |
123 | Message::rawParam( Linker::makeExternalLink( $jumpUrl, $wiki ) ) |
124 | ); |
125 | |
126 | return; |
127 | } |
128 | } else { |
129 | $out->setPageTitleMsg( $this->msg( 'securepoll-create-title' ) ); |
130 | if ( !$this->specialPage->getUser()->isAllowed( 'securepoll-create-poll' ) ) { |
131 | throw new PermissionsError( 'securepoll-create-poll' ); |
132 | } |
133 | } |
134 | |
135 | $out->addJsConfigVars( 'SecurePollSubPage', 'create' ); |
136 | $out->addModules( 'ext.securepoll.htmlform' ); |
137 | $out->addModuleStyles( [ |
138 | 'mediawiki.widgets.TagMultiselectWidget.styles', |
139 | 'ext.securepoll', |
140 | ] ); |
141 | |
142 | $election = $this->election; |
143 | $isRunning = $election && $election->isStarted() && !$election->isFinished(); |
144 | $formItems = []; |
145 | |
146 | $formItems['election_id'] = [ |
147 | 'type' => 'hidden', |
148 | 'default' => -1, |
149 | 'output-as-default' => false, |
150 | ]; |
151 | |
152 | // Submit intended to be hidden w/CSS |
153 | // Placed at the beginning of the form so that when the form |
154 | // is submitted by pressing enter while focused on an input, |
155 | // it will trigger this generic submit and not generate an event |
156 | // on a cloner add/delete item |
157 | $formItems['default_submit'] = [ |
158 | 'type' => 'submit', |
159 | 'buttonlabel' => 'submit', |
160 | 'cssclass' => 'securepoll-default-submit', |
161 | ]; |
162 | |
163 | $formItems['election_title'] = [ |
164 | 'label-message' => 'securepoll-create-label-election_title', |
165 | 'type' => 'text', |
166 | 'required' => true, |
167 | 'disabled' => $isRunning, |
168 | ]; |
169 | |
170 | $wikiNames = FormStore::getWikiList(); |
171 | $options = []; |
172 | $options['securepoll-create-option-wiki-this_wiki'] = WikiMap::getCurrentWikiId(); |
173 | if ( count( $wikiNames ) > 1 ) { |
174 | $options['securepoll-create-option-wiki-all_wikis'] = '*'; |
175 | } |
176 | $securePollCreateWikiGroupDir = $this->specialPage->getConfig()->get( 'SecurePollCreateWikiGroupDir' ); |
177 | foreach ( $this->specialPage->getConfig()->get( 'SecurePollCreateWikiGroups' ) as $file => $msg ) { |
178 | if ( is_readable( "$securePollCreateWikiGroupDir$file.dblist" ) ) { |
179 | $options[$msg] = "@$file"; |
180 | } |
181 | } |
182 | |
183 | // If the only option is WikiMap::getCurrentWikiId() don't show it; otherwise... |
184 | if ( count( $wikiNames ) > 1 || count( $options ) > 1 ) { |
185 | $opts = []; |
186 | foreach ( $options as $msg => $value ) { |
187 | $opts[$this->msg( $msg )->plain()] = $value; |
188 | } |
189 | $key = array_search( WikiMap::getCurrentWikiId(), $wikiNames, true ); |
190 | if ( $key !== false ) { |
191 | unset( $wikiNames[$key] ); |
192 | } |
193 | if ( $wikiNames ) { |
194 | $opts[$this->msg( 'securepoll-create-option-wiki-other_wiki' )->plain()] = $wikiNames; |
195 | } |
196 | $formItems['property_wiki'] = [ |
197 | 'type' => 'select', |
198 | 'options' => $opts, |
199 | 'label-message' => 'securepoll-create-label-wiki', |
200 | 'disabled' => $isRunning, |
201 | ]; |
202 | } |
203 | |
204 | $languages = $this->languageNameUtils->getLanguageNames(); |
205 | ksort( $languages ); |
206 | $options = []; |
207 | foreach ( $languages as $code => $name ) { |
208 | $display = LanguageCode::bcp47( $code ) . ' - ' . $name; |
209 | $options[$display] = $code; |
210 | } |
211 | $formItems['election_primaryLang'] = [ |
212 | 'type' => 'select', |
213 | 'options' => $options, |
214 | 'label-message' => 'securepoll-create-label-election_primarylang', |
215 | 'default' => 'en', |
216 | 'required' => true, |
217 | 'disabled' => $isRunning, |
218 | ]; |
219 | |
220 | $formItems['election_startdate'] = [ |
221 | 'label-message' => 'securepoll-create-label-election_startdate', |
222 | 'type' => 'datetime', |
223 | 'required' => true, |
224 | 'min' => $isRunning ? '' : gmdate( 'Y-m-d H:i:s' ), |
225 | 'disabled' => $isRunning, |
226 | ]; |
227 | |
228 | $formItems['election_enddate'] = [ |
229 | 'label-message' => 'securepoll-create-label-election_enddate', |
230 | 'type' => 'datetime', |
231 | 'required' => true, |
232 | 'min' => $isRunning ? '' : gmdate( 'Y-m-d H:i:s' ), |
233 | 'validation-callback' => [ |
234 | $this, |
235 | 'checkElectionEndDate' |
236 | ], |
237 | 'disabled' => $isRunning, |
238 | ]; |
239 | |
240 | $formItems['return-url'] = [ |
241 | 'label-message' => 'securepoll-create-label-election_return-url', |
242 | 'type' => 'url', |
243 | ]; |
244 | |
245 | if ( isset( $formItems['property_wiki'] ) ) { |
246 | $formItems['jump-text'] = [ |
247 | 'label-message' => 'securepoll-create-label-election_jump-text', |
248 | 'type' => 'text', |
249 | 'disabled' => $isRunning, |
250 | ]; |
251 | $formItems['jump-text']['hide-if'] = [ |
252 | '===', |
253 | 'property_wiki', |
254 | WikiMap::getCurrentWikiId() |
255 | ]; |
256 | } |
257 | |
258 | $formItems['election_type'] = [ |
259 | 'label-message' => 'securepoll-create-label-election_type', |
260 | 'type' => 'radio', |
261 | 'options-messages' => [], |
262 | 'required' => true, |
263 | 'disabled' => $isRunning, |
264 | ]; |
265 | |
266 | $cryptTypes = Crypt::getCryptTypes(); |
267 | if ( count( $cryptTypes ) > 1 ) { |
268 | $formItems['election_crypt'] = [ |
269 | 'label-message' => 'securepoll-create-label-election_crypt', |
270 | 'type' => 'radio', |
271 | 'options-messages' => [], |
272 | 'required' => true, |
273 | 'disabled' => $isRunning, |
274 | ]; |
275 | } else { |
276 | reset( $cryptTypes ); |
277 | $formItems['election_crypt'] = [ |
278 | 'type' => 'hidden', |
279 | 'default' => key( $cryptTypes ), |
280 | 'options-messages' => [], |
281 | // dummy, ignored |
282 | ]; |
283 | } |
284 | |
285 | $formItems['disallow-change'] = [ |
286 | 'label-message' => 'securepoll-create-label-election_disallow-change', |
287 | 'type' => 'check', |
288 | 'hidelabel' => true, |
289 | 'disabled' => $isRunning, |
290 | ]; |
291 | |
292 | $formItems['voter-privacy'] = [ |
293 | 'label-message' => 'securepoll-create-label-voter_privacy', |
294 | 'type' => 'check', |
295 | 'hidelabel' => true, |
296 | 'disabled' => $isRunning, |
297 | ]; |
298 | |
299 | $formItems['property_admins'] = [ |
300 | 'label-message' => 'securepoll-create-label-property_admins', |
301 | 'type' => 'usersmultiselect', |
302 | 'exists' => true, |
303 | 'required' => true, |
304 | 'validation-callback' => [ |
305 | $this, |
306 | 'checkIfInElectionAdminUserGroup' |
307 | ], |
308 | ]; |
309 | |
310 | $formItems['request-comment'] = [ |
311 | 'label-message' => 'securepoll-create-label-request-comment', |
312 | 'type' => 'check', |
313 | 'disabled' => $isRunning |
314 | ]; |
315 | |
316 | $formItems['comment-prompt'] = [ |
317 | 'label-message' => 'securepoll-create-label-comment-prompt', |
318 | 'type' => 'textarea', |
319 | 'rows' => 2, |
320 | 'disabled' => $isRunning, |
321 | 'hide-if' => [ |
322 | '!==', |
323 | 'request-comment', |
324 | '1' |
325 | ] |
326 | ]; |
327 | |
328 | $questionFields = [ |
329 | 'id' => [ |
330 | 'type' => 'hidden', |
331 | 'default' => -1, |
332 | 'output-as-default' => false, |
333 | ], |
334 | 'text' => [ |
335 | 'label-message' => 'securepoll-create-label-questions-question', |
336 | 'type' => 'text', |
337 | 'validation-callback' => [ |
338 | $this, |
339 | 'checkRequired', |
340 | ], |
341 | 'disabled' => $isRunning, |
342 | ], |
343 | 'delete' => [ |
344 | 'type' => 'submit', |
345 | 'default' => $this->msg( 'securepoll-create-label-questions-delete' )->text(), |
346 | 'disabled' => $isRunning, |
347 | 'flags' => [ |
348 | 'destructive' |
349 | ], |
350 | ], |
351 | ]; |
352 | |
353 | $optionFields = [ |
354 | 'id' => [ |
355 | 'type' => 'hidden', |
356 | 'default' => -1, |
357 | 'output-as-default' => false, |
358 | ], |
359 | 'text' => [ |
360 | 'label-message' => 'securepoll-create-label-options-option', |
361 | 'type' => 'text', |
362 | 'validation-callback' => [ |
363 | $this, |
364 | 'checkRequired', |
365 | ], |
366 | 'disabled' => $isRunning, |
367 | ], |
368 | 'delete' => [ |
369 | 'type' => 'submit', |
370 | 'default' => $this->msg( 'securepoll-create-label-options-delete' )->text(), |
371 | 'disabled' => $isRunning, |
372 | 'flags' => [ |
373 | 'destructive' |
374 | ], |
375 | ], |
376 | ]; |
377 | |
378 | $tallyTypes = []; |
379 | foreach ( $this->context->getBallotTypesForVote() as $ballotType => $ballotClass ) { |
380 | $types = []; |
381 | foreach ( |
382 | call_user_func_array( [ $ballotClass, 'getTallyTypes' ], [] ) as $tallyType |
383 | ) { |
384 | $type = "$ballotType+$tallyType"; |
385 | $types[] = $type; |
386 | $tallyTypes[$tallyType][] = $type; |
387 | $formItems['election_type']['options-messages']["securepoll-create-option-election_type-$type"] |
388 | = $type; |
389 | } |
390 | |
391 | self::processFormItems( |
392 | $formItems, |
393 | 'election_type', |
394 | $types, |
395 | $ballotClass, |
396 | 'election', |
397 | $isRunning |
398 | ); |
399 | self::processFormItems( |
400 | $questionFields, |
401 | 'election_type', |
402 | $types, |
403 | $ballotClass, |
404 | 'question', |
405 | $isRunning |
406 | ); |
407 | self::processFormItems( |
408 | $optionFields, |
409 | 'election_type', |
410 | $types, |
411 | $ballotClass, |
412 | 'option', |
413 | $isRunning |
414 | ); |
415 | } |
416 | |
417 | foreach ( Tallier::$tallierTypes as $type => $class ) { |
418 | if ( !isset( $tallyTypes[$type] ) ) { |
419 | continue; |
420 | } |
421 | self::processFormItems( |
422 | $formItems, |
423 | 'election_type', |
424 | $tallyTypes[$type], |
425 | $class, |
426 | 'election', |
427 | $isRunning |
428 | ); |
429 | self::processFormItems( |
430 | $questionFields, |
431 | 'election_type', |
432 | $tallyTypes[$type], |
433 | $class, |
434 | 'question', |
435 | $isRunning |
436 | ); |
437 | self::processFormItems( |
438 | $optionFields, |
439 | 'election_type', |
440 | $tallyTypes[$type], |
441 | $class, |
442 | 'option', |
443 | $isRunning |
444 | ); |
445 | } |
446 | |
447 | foreach ( Crypt::getCryptTypes() as $type => $class ) { |
448 | $formItems['election_crypt']['options-messages']["securepoll-create-option-election_crypt-$type"] |
449 | = $type; |
450 | if ( $class !== false ) { |
451 | self::processFormItems( |
452 | $formItems, |
453 | 'election_crypt', |
454 | $type, |
455 | $class, |
456 | 'election', |
457 | $isRunning |
458 | ); |
459 | self::processFormItems( |
460 | $questionFields, |
461 | 'election_crypt', |
462 | $type, |
463 | $class, |
464 | 'question', |
465 | $isRunning |
466 | ); |
467 | self::processFormItems( |
468 | $optionFields, |
469 | 'election_crypt', |
470 | $type, |
471 | $class, |
472 | 'option', |
473 | $isRunning |
474 | ); |
475 | } |
476 | } |
477 | |
478 | $questionFields['options'] = [ |
479 | 'label-message' => 'securepoll-create-label-questions-option', |
480 | 'type' => 'cloner', |
481 | 'required' => true, |
482 | 'create-button-message' => 'securepoll-create-label-options-add', |
483 | 'fields' => $optionFields, |
484 | 'disabled' => $isRunning, |
485 | ]; |
486 | |
487 | $formItems['questions'] = [ |
488 | 'label-message' => 'securepoll-create-label-questions', |
489 | 'type' => 'cloner', |
490 | 'row-legend' => 'securepoll-create-questions-row-legend', |
491 | 'create-button-message' => 'securepoll-create-label-questions-add', |
492 | 'fields' => $questionFields, |
493 | 'disabled' => $isRunning, |
494 | ]; |
495 | |
496 | if ( $this->specialPage->getConfig()->get( 'SecurePollUseNamespace' ) ) { |
497 | $formItems['comment'] = [ |
498 | 'type' => 'text', |
499 | 'label-message' => 'securepoll-create-label-comment', |
500 | 'maxlength' => 250, |
501 | ]; |
502 | } |
503 | |
504 | // Set form field defaults from any existing election |
505 | if ( $this->election ) { |
506 | $existingFieldData = $this->getFormDataFromElection(); |
507 | foreach ( $existingFieldData as $fieldName => $fieldValue ) { |
508 | if ( isset( $formItems[ $fieldName ] ) ) { |
509 | $formItems[ $fieldName ]['default'] = $fieldValue; |
510 | } |
511 | } |
512 | } |
513 | |
514 | $form = HTMLForm::factory( |
515 | 'ooui', |
516 | $formItems, |
517 | $this->specialPage->getContext(), |
518 | $this->election ? 'securepoll-edit' : 'securepoll-create' |
519 | ); |
520 | |
521 | $form->setSubmitTextMsg( |
522 | $this->election ? 'securepoll-edit-action' : 'securepoll-create-action' |
523 | ); |
524 | $form->setSubmitCallback( |
525 | [ |
526 | $this, |
527 | $isRunning ? 'processInputDuringElection' : 'processInput' |
528 | ] |
529 | ); |
530 | $form->prepareForm(); |
531 | |
532 | // If this isn't the result of a POST, load the data from the election |
533 | $request = $this->specialPage->getRequest(); |
534 | if ( $this->election && !( $request->wasPosted() && $request->getCheck( |
535 | 'wpEditToken' |
536 | ) ) |
537 | ) { |
538 | $form->mFieldData = $this->getFormDataFromElection(); |
539 | } |
540 | |
541 | $result = $form->tryAuthorizedSubmit(); |
542 | if ( $result === true || ( $result instanceof Status && $result->isGood() ) ) { |
543 | if ( $this->election ) { |
544 | $out->setPageTitleMsg( $this->msg( 'securepoll-edit-edited' ) ); |
545 | $out->addWikiMsg( 'securepoll-edit-edited-text' ); |
546 | } else { |
547 | $out->setPageTitleMsg( $this->msg( 'securepoll-create-created' ) ); |
548 | $out->addWikiMsg( 'securepoll-create-created-text' ); |
549 | } |
550 | $out->returnToMain( false, SpecialPage::getTitleFor( 'SecurePoll' ) ); |
551 | } else { |
552 | $form->displayForm( $result ); |
553 | } |
554 | } |
555 | |
556 | public function processInputDuringElection( $formData ) { |
557 | // If editing a poll while it's running, only allow certain fields to be updated |
558 | // For now only property_admins and return-url can be edited |
559 | $fields = [ |
560 | 'admins' => implode( '|', explode( "\n", $formData['property_admins'] ) ), |
561 | 'return-url' => $formData['return-url'] |
562 | ]; |
563 | |
564 | $originalFormData = []; |
565 | $securePollUseLogging = $this->specialPage->getConfig()->get( 'SecurePollUseLogging' ); |
566 | if ( $securePollUseLogging ) { |
567 | // Store original form data for logging |
568 | $originalFormData = $this->getFormDataFromElection(); |
569 | } |
570 | |
571 | $dbw = $this->lbFactory->getMainLB()->getConnection( ILoadBalancer::DB_PRIMARY ); |
572 | $dbw->startAtomic( __METHOD__ ); |
573 | foreach ( $fields as $pr_key => $pr_value ) { |
574 | $dbw->newUpdateQueryBuilder() |
575 | ->update( 'securepoll_properties' ) |
576 | ->set( [ 'pr_value' => $pr_value ] ) |
577 | ->where( [ |
578 | 'pr_entity' => $this->election->getId(), |
579 | 'pr_key' => $pr_key |
580 | ] ) |
581 | ->caller( __METHOD__ ) |
582 | ->execute(); |
583 | } |
584 | $dbw->endAtomic( __METHOD__ ); |
585 | |
586 | // Log any changes to admins |
587 | if ( $securePollUseLogging ) { |
588 | $this->logAdminChanges( $originalFormData, $formData, $this->election->getId() ); |
589 | } |
590 | |
591 | $this->recordElectionToNamespace( $this->election->getId(), $formData ); |
592 | |
593 | return Status::newGood( $this->election->getId() ); |
594 | } |
595 | |
596 | public function processInput( $formData, $form ) { |
597 | try { |
598 | $context = new Context; |
599 | $userId = $this->specialPage->getUser()->getId(); |
600 | $store = new FormStore; |
601 | $context->setStore( $store ); |
602 | $store->setFormData( $context, $formData, $userId ); |
603 | $election = $context->getElection( $store->eId ); |
604 | |
605 | if ( $this->election && $store->eId !== (int)$this->election->getId() ) { |
606 | return Status::newFatal( 'securepoll-create-fail-bad-id' ); |
607 | } |
608 | |
609 | // Get a connection in autocommit mode so that it is possible to do |
610 | // explicit transactions on it (T287859) |
611 | $dbw = $this->lbFactory->getMainLB()->getConnection( ILoadBalancer::DB_PRIMARY, |
612 | [], false, ILoadBalancer::CONN_TRX_AUTOCOMMIT ); |
613 | |
614 | // Check for duplicate titles on the local wiki |
615 | $id = $dbw->newSelectQueryBuilder() |
616 | ->select( 'el_entity' ) |
617 | ->from( 'securepoll_elections' ) |
618 | ->where( [ 'el_title' => $election->title ] ) |
619 | ->caller( __METHOD__ ) |
620 | ->fetchField(); |
621 | if ( $id && (int)$id !== $election->getId() ) { |
622 | throw new StatusException( |
623 | 'securepoll-create-duplicate-title', |
624 | FormStore::getWikiName( WikiMap::getCurrentWikiId() ), |
625 | WikiMap::getCurrentWikiId() |
626 | ); |
627 | } |
628 | |
629 | // Check for duplicate titles on jump wikis too |
630 | // (There's the possibility for a race here, but hopefully it won't |
631 | // matter in practice) |
632 | if ( $store->rId ) { |
633 | foreach ( $store->remoteWikis as $dbname ) { |
634 | $lb = $this->lbFactory->getMainLB( $dbname ); |
635 | // Use autocommit mode so that we can share connections with |
636 | // the write code below |
637 | $rdbw = $lb->getConnection( DB_PRIMARY, [], $dbname, |
638 | ILoadBalancer::CONN_TRX_AUTOCOMMIT ); |
639 | |
640 | // Find an existing dummy election, if any |
641 | $rId = $rdbw->newSelectQueryBuilder() |
642 | ->select( 'p1.pr_entity' ) |
643 | ->from( 'securepoll_properties', 'p1' ) |
644 | ->join( 'securepoll_properties', 'p2', 'p1.pr_entity = p2.pr_entity' ) |
645 | ->where( [ |
646 | 'p1.pr_key' => 'jump-id', |
647 | 'p1.pr_value' => $election->getId(), |
648 | 'p2.pr_key' => 'main-wiki', |
649 | 'p2.pr_value' => WikiMap::getCurrentWikiId(), |
650 | ] ) |
651 | ->caller( __METHOD__ ) |
652 | ->fetchField(); |
653 | // Test for duplicate title |
654 | $id = $rdbw->newSelectQueryBuilder() |
655 | ->select( 'el_entity' ) |
656 | ->from( 'securepoll_elections' ) |
657 | ->where( [ |
658 | 'el_title' => $formData['election_title'] |
659 | ] ) |
660 | ->caller( __METHOD__ ) |
661 | ->fetchField(); |
662 | |
663 | if ( $id && $id !== $rId ) { |
664 | throw new StatusException( |
665 | 'securepoll-create-duplicate-title', |
666 | FormStore::getWikiName( $dbname ), |
667 | $dbname |
668 | ); |
669 | } |
670 | } |
671 | } |
672 | } catch ( StatusException $ex ) { |
673 | return $ex->status; |
674 | } |
675 | |
676 | $originalFormData = []; |
677 | $securePollUseLogging = $this->specialPage->getConfig()->get( 'SecurePollUseLogging' ); |
678 | if ( $securePollUseLogging && $this->election ) { |
679 | // Store original form data for logging |
680 | $originalFormData = $this->getFormDataFromElection(); |
681 | } |
682 | |
683 | // Ok, begin the actual work |
684 | $dbw->startAtomic( __METHOD__ ); |
685 | if ( $election->getId() > 0 ) { |
686 | $id = $dbw->newSelectQueryBuilder() |
687 | ->select( 'el_entity' ) |
688 | ->from( 'securepoll_elections' ) |
689 | ->where( [ |
690 | 'el_entity' => $election->getId() |
691 | ] ) |
692 | ->forUpdate() |
693 | ->caller( __METHOD__ ) |
694 | ->fetchField(); |
695 | if ( !$id ) { |
696 | $dbw->endAtomic( __METHOD__ ); |
697 | |
698 | return Status::newFatal( 'securepoll-create-fail-id-missing' ); |
699 | } |
700 | } |
701 | |
702 | // Insert or update the election entity |
703 | $fields = [ |
704 | 'el_title' => $election->title, |
705 | 'el_ballot' => $election->ballotType, |
706 | 'el_tally' => $election->tallyType, |
707 | 'el_primary_lang' => $election->getLanguage(), |
708 | 'el_start_date' => $dbw->timestamp( $election->getStartDate() ), |
709 | 'el_end_date' => $dbw->timestamp( $election->getEndDate() ), |
710 | 'el_auth_type' => $election->authType, |
711 | 'el_owner' => $election->owner, |
712 | ]; |
713 | if ( $election->getId() < 0 ) { |
714 | $eId = self::insertEntity( $dbw, 'election' ); |
715 | $qIds = []; |
716 | $oIds = []; |
717 | $fields['el_entity'] = $eId; |
718 | $dbw->newInsertQueryBuilder() |
719 | ->insertInto( 'securepoll_elections' ) |
720 | ->row( $fields ) |
721 | ->caller( __METHOD__ ) |
722 | ->execute(); |
723 | |
724 | // Enable sitewide block by default on new elections |
725 | $dbw->newInsertQueryBuilder() |
726 | ->insertInto( 'securepoll_properties' ) |
727 | ->row( [ |
728 | 'pr_entity' => $eId, |
729 | 'pr_key' => 'not-sitewide-blocked', |
730 | 'pr_value' => 1, |
731 | ] ) |
732 | ->caller( __METHOD__ ) |
733 | ->execute(); |
734 | } else { |
735 | $eId = $election->getId(); |
736 | $dbw->newUpdateQueryBuilder() |
737 | ->update( 'securepoll_elections' ) |
738 | ->set( $fields ) |
739 | ->where( [ 'el_entity' => $eId ] ) |
740 | ->caller( __METHOD__ ) |
741 | ->execute(); |
742 | |
743 | // Delete any questions or options that weren't included in the |
744 | // form submission. |
745 | $qIds = $dbw->newSelectQueryBuilder() |
746 | ->select( 'qu_entity' ) |
747 | ->from( 'securepoll_questions' ) |
748 | ->where( [ 'qu_election' => $eId ] ) |
749 | ->caller( __METHOD__ ) |
750 | ->fetchFieldValues(); |
751 | $oIds = $dbw->newSelectQueryBuilder() |
752 | ->select( 'op_entity' ) |
753 | ->from( 'securepoll_options' ) |
754 | ->where( [ 'op_election' => $eId ] ) |
755 | ->caller( __METHOD__ ) |
756 | ->fetchFieldValues(); |
757 | $deleteIds = array_merge( |
758 | array_diff( $qIds, $store->qIds ), |
759 | array_diff( $oIds, $store->oIds ) |
760 | ); |
761 | if ( $deleteIds ) { |
762 | $dbw->newDeleteQueryBuilder() |
763 | ->deleteFrom( 'securepoll_msgs' ) |
764 | ->where( [ 'msg_entity' => $deleteIds ] ) |
765 | ->caller( __METHOD__ ) |
766 | ->execute(); |
767 | $dbw->newDeleteQueryBuilder() |
768 | ->deleteFrom( 'securepoll_properties' ) |
769 | ->where( [ 'pr_entity' => $deleteIds ] ) |
770 | ->caller( __METHOD__ ) |
771 | ->execute(); |
772 | $dbw->newDeleteQueryBuilder() |
773 | ->deleteFrom( 'securepoll_questions' ) |
774 | ->where( [ 'qu_entity' => $deleteIds ] ) |
775 | ->caller( __METHOD__ ) |
776 | ->execute(); |
777 | $dbw->newDeleteQueryBuilder() |
778 | ->deleteFrom( 'securepoll_options' ) |
779 | ->where( [ 'op_entity' => $deleteIds ] ) |
780 | ->caller( __METHOD__ ) |
781 | ->execute(); |
782 | $dbw->newDeleteQueryBuilder() |
783 | ->deleteFrom( 'securepoll_entity' ) |
784 | ->where( [ 'en_id' => $deleteIds ] ) |
785 | ->caller( __METHOD__ ) |
786 | ->execute(); |
787 | } |
788 | } |
789 | self::savePropertiesAndMessages( $dbw, $eId, $election ); |
790 | |
791 | // Now do questions and options |
792 | $qIndex = 0; |
793 | foreach ( $election->getQuestions() as $question ) { |
794 | $qId = $question->getId(); |
795 | if ( !in_array( $qId, $qIds ) ) { |
796 | $qId = self::insertEntity( $dbw, 'question' ); |
797 | } |
798 | $dbw->newReplaceQueryBuilder() |
799 | ->replaceInto( 'securepoll_questions' ) |
800 | ->uniqueIndexFields( 'qu_entity' ) |
801 | ->row( [ |
802 | 'qu_entity' => $qId, |
803 | 'qu_election' => $eId, |
804 | 'qu_index' => ++$qIndex, |
805 | ] ) |
806 | ->caller( __METHOD__ ) |
807 | ->execute(); |
808 | self::savePropertiesAndMessages( $dbw, $qId, $question ); |
809 | |
810 | foreach ( $question->getOptions() as $option ) { |
811 | $oId = $option->getId(); |
812 | if ( !in_array( $oId, $oIds ) ) { |
813 | $oId = self::insertEntity( $dbw, 'option' ); |
814 | } |
815 | $dbw->newReplaceQueryBuilder() |
816 | ->replaceInto( 'securepoll_options' ) |
817 | ->uniqueIndexFields( 'op_entity' ) |
818 | ->row( [ |
819 | 'op_entity' => $oId, |
820 | 'op_election' => $eId, |
821 | 'op_question' => $qId, |
822 | ] ) |
823 | ->caller( __METHOD__ ) |
824 | ->execute(); |
825 | self::savePropertiesAndMessages( $dbw, $oId, $option ); |
826 | } |
827 | } |
828 | $dbw->endAtomic( __METHOD__ ); |
829 | |
830 | if ( $securePollUseLogging ) { |
831 | $this->logAdminChanges( $originalFormData, $formData, $eId ); |
832 | } |
833 | |
834 | // Create the "redirect" polls on foreign wikis |
835 | if ( $store->rId ) { |
836 | $election = $context->getElection( $store->rId ); |
837 | foreach ( $store->remoteWikis as $dbname ) { |
838 | $lb = $this->lbFactory->getMainLB( $dbname ); |
839 | // As for the local wiki, request autocommit mode to get outer transaction scope |
840 | $dbw = $lb->getConnection( ILoadBalancer::DB_PRIMARY, [], $dbname, |
841 | ILoadBalancer::CONN_TRX_AUTOCOMMIT ); |
842 | $dbw->startAtomic( __METHOD__ ); |
843 | // Find an existing dummy election, if any |
844 | $rId = $dbw->newSelectQueryBuilder() |
845 | ->select( 'p1.pr_entity' ) |
846 | ->from( 'securepoll_properties', 'p1' ) |
847 | ->join( 'securepoll_properties', 'p2', 'p1.pr_entity = p2.pr_entity' ) |
848 | ->where( [ |
849 | 'p1.pr_key' => 'jump-id', |
850 | 'p1.pr_value' => $eId, |
851 | 'p2.pr_key' => 'main-wiki', |
852 | 'p2.pr_value' => WikiMap::getCurrentWikiId(), |
853 | ] ) |
854 | ->caller( __METHOD__ ) |
855 | ->fetchField(); |
856 | if ( !$rId ) { |
857 | $rId = self::insertEntity( $dbw, 'election' ); |
858 | } |
859 | |
860 | // Insert it! We don't have to care about questions or options here. |
861 | $dbw->newReplaceQueryBuilder() |
862 | ->replaceInto( 'securepoll_elections' ) |
863 | ->uniqueIndexFields( 'el_entity' ) |
864 | ->row( [ |
865 | 'el_entity' => $rId, |
866 | 'el_title' => $election->title, |
867 | 'el_ballot' => $election->ballotType, |
868 | 'el_tally' => $election->tallyType, |
869 | 'el_primary_lang' => $election->getLanguage(), |
870 | 'el_start_date' => $dbw->timestamp( $election->getStartDate() ), |
871 | 'el_end_date' => $dbw->timestamp( $election->getEndDate() ), |
872 | 'el_auth_type' => $election->authType, |
873 | 'el_owner' => $election->owner, |
874 | ] ) |
875 | ->caller( __METHOD__ ) |
876 | ->execute(); |
877 | self::savePropertiesAndMessages( $dbw, $rId, $election ); |
878 | |
879 | // Fix jump-id |
880 | $dbw->newUpdateQueryBuilder() |
881 | ->update( 'securepoll_properties' ) |
882 | ->set( [ 'pr_value' => $eId ] ) |
883 | ->where( [ |
884 | 'pr_entity' => $rId, |
885 | 'pr_key' => 'jump-id' |
886 | ] ) |
887 | ->caller( __METHOD__ ) |
888 | ->execute(); |
889 | $dbw->endAtomic( __METHOD__ ); |
890 | } |
891 | } |
892 | |
893 | $this->recordElectionToNamespace( $eId, $formData ); |
894 | |
895 | return Status::newGood( $eId ); |
896 | } |
897 | |
898 | /** |
899 | * Record this election to the SecurePoll namespace, if so configured. |
900 | * |
901 | * @param int $eId election id |
902 | * @param array $formData |
903 | */ |
904 | private function recordElectionToNamespace( $eId, $formData ) { |
905 | if ( $this->specialPage->getConfig()->get( 'SecurePollUseNamespace' ) ) { |
906 | // Create a new context to bypass caching. |
907 | $context = new Context; |
908 | // We may be inside a transaction, so force a primary DB connection (T209804) |
909 | $context->getStore()->setForcePrimary( true ); |
910 | |
911 | $election = $context->getElection( $eId ); |
912 | |
913 | [ $title, $content ] = SecurePollContentHandler::makeContentFromElection( |
914 | $election |
915 | ); |
916 | $wp = $this->wikiPageFactory->newFromTitle( $title ); |
917 | $wp->doUserEditContent( |
918 | $content, |
919 | $this->specialPage->getUser(), |
920 | $formData['comment'] |
921 | ); |
922 | |
923 | [ $title, $content ] = SecurePollContentHandler::makeContentFromElection( |
924 | $election, |
925 | 'msg/' . $election->getLanguage() |
926 | ); |
927 | $wp = $this->wikiPageFactory->newFromTitle( $title ); |
928 | $wp->doUserEditContent( |
929 | $content, |
930 | $this->specialPage->getUser(), |
931 | $formData['comment'] |
932 | ); |
933 | } |
934 | } |
935 | |
936 | /** |
937 | * Log changes made to the admins of the election. |
938 | * |
939 | * @param array $originalFormData Empty array if no election exists |
940 | * @param array $formData |
941 | * @param int $electionId |
942 | */ |
943 | private function logAdminChanges( |
944 | array $originalFormData, |
945 | array $formData, |
946 | int $electionId |
947 | ): void { |
948 | if ( isset( $originalFormData['property_admins'] ) ) { |
949 | $oldAdmins = explode( "\n", $originalFormData['property_admins'] ); |
950 | } else { |
951 | $oldAdmins = []; |
952 | } |
953 | $newAdmins = explode( "\n", $formData['property_admins'] ); |
954 | |
955 | if ( $oldAdmins === $newAdmins ) { |
956 | return; |
957 | } |
958 | |
959 | $actions = [ |
960 | self::LOG_TYPE_ADDADMIN => array_diff( $newAdmins, $oldAdmins ), |
961 | self::LOG_TYPE_REMOVEADMIN => array_diff( $oldAdmins, $newAdmins ), |
962 | ]; |
963 | |
964 | $dbw = $this->lbFactory->getMainLB()->getConnection( ILoadBalancer::DB_PRIMARY ); |
965 | $fields = [ |
966 | 'spl_timestamp' => $dbw->timestamp( time() ), |
967 | 'spl_election_id' => $electionId, |
968 | 'spl_user' => $this->specialPage->getUser()->getId(), |
969 | ]; |
970 | |
971 | foreach ( array_keys( $actions ) as $action ) { |
972 | foreach ( $actions[$action] as $admin ) { |
973 | $dbw->newInsertQueryBuilder() |
974 | ->insertInto( 'securepoll_log' ) |
975 | ->row( $fields + [ |
976 | 'spl_type' => $action, |
977 | 'spl_target' => $this->userFactory->newFromName( $admin )->getId(), |
978 | ] ) |
979 | ->caller( __METHOD__ ) |
980 | ->execute(); |
981 | } |
982 | } |
983 | } |
984 | |
985 | /** |
986 | * Recreate the form data from an election |
987 | * |
988 | * @return array |
989 | */ |
990 | private function getFormDataFromElection() { |
991 | $lang = $this->election->getLanguage(); |
992 | $data = array_replace_recursive( |
993 | SecurePollContentHandler::getDataFromElection( $this->election, "msg/$lang" ), |
994 | SecurePollContentHandler::getDataFromElection( $this->election ) |
995 | ); |
996 | $p = &$data['properties']; |
997 | $m = &$data['messages']; |
998 | |
999 | $startDate = new MWTimestamp( $data['startDate'] ); |
1000 | $endDate = new MWTimestamp( $data['endDate'] ); |
1001 | |
1002 | $ballot = $data['ballot']; |
1003 | $tally = $data['tally']; |
1004 | $crypt = $p['encrypt-type'] ?? 'none'; |
1005 | |
1006 | $formData = [ |
1007 | 'election_id' => $data['id'], |
1008 | 'election_title' => $data['title'], |
1009 | 'property_wiki' => $p['wikis-val'] ?? null, |
1010 | 'election_primaryLang' => $data['lang'], |
1011 | 'election_startdate' => $startDate->format( 'Y-m-d\TH:i:s.0\Z' ), |
1012 | 'election_enddate' => $endDate->format( 'Y-m-d\TH:i:s.0\Z' ), |
1013 | 'return-url' => $p['return-url'] ?? null, |
1014 | 'jump-text' => $m['jump-text'] ?? null, |
1015 | 'election_type' => "{$ballot}+{$tally}", |
1016 | 'election_crypt' => $crypt, |
1017 | 'disallow-change' => isset( $p['disallow-change'] ) ? (bool)$p['disallow-change'] : null, |
1018 | 'voter-privacy' => isset( $p['voter-privacy'] ) ? (bool)$p['voter-privacy'] : null, |
1019 | 'property_admins' => '', |
1020 | 'request-comment' => isset( $p['request-comment'] ) ? (bool)$p['request-comment'] : null, |
1021 | 'comment-prompt' => $m['comment-prompt'] ?? null, |
1022 | 'questions' => [], |
1023 | 'comment' => '', |
1024 | ]; |
1025 | |
1026 | if ( isset( $data['properties']['admins'] ) ) { |
1027 | // HTMLUsersMultiselectField takes a line-separated string |
1028 | $formData['property_admins'] = implode( "\n", explode( '|', $data['properties']['admins'] ) ); |
1029 | } |
1030 | |
1031 | $classes = []; |
1032 | $tallyTypes = []; |
1033 | foreach ( $this->context->getBallotTypesForVote() as $class ) { |
1034 | $classes[] = $class; |
1035 | foreach ( call_user_func_array( [ $class, 'getTallyTypes' ], [] ) as $type ) { |
1036 | $tallyTypes[$type] = true; |
1037 | } |
1038 | } |
1039 | foreach ( Tallier::$tallierTypes as $type => $class ) { |
1040 | if ( isset( $tallyTypes[$type] ) ) { |
1041 | $classes[] = $class; |
1042 | } |
1043 | } |
1044 | foreach ( Crypt::getCryptTypes() as $class ) { |
1045 | if ( $class !== false ) { |
1046 | $classes[] = $class; |
1047 | } |
1048 | } |
1049 | |
1050 | foreach ( $classes as $class ) { |
1051 | self::unprocessFormData( $formData, $data, $class, 'election' ); |
1052 | } |
1053 | |
1054 | foreach ( $data['questions'] as $question ) { |
1055 | $q = [ |
1056 | 'text' => $question['messages']['text'], |
1057 | ]; |
1058 | if ( isset( $question['id'] ) ) { |
1059 | $q['id'] = $question['id']; |
1060 | } |
1061 | |
1062 | foreach ( $classes as $class ) { |
1063 | self::unprocessFormData( $q, $question, $class, 'question' ); |
1064 | } |
1065 | |
1066 | // Process options for this question |
1067 | foreach ( $question['options'] as $option ) { |
1068 | $o = [ |
1069 | 'text' => $option['messages']['text'], |
1070 | ]; |
1071 | if ( isset( $option['id'] ) ) { |
1072 | $o['id'] = $option['id']; |
1073 | } |
1074 | |
1075 | foreach ( $classes as $class ) { |
1076 | self::unprocessFormData( $o, $option, $class, 'option' ); |
1077 | } |
1078 | |
1079 | $q['options'][] = $o; |
1080 | } |
1081 | |
1082 | $formData['questions'][] = $q; |
1083 | } |
1084 | |
1085 | return $formData; |
1086 | } |
1087 | |
1088 | /** |
1089 | * Insert an entry into the securepoll_entities table, and return the ID |
1090 | * |
1091 | * @param IDatabase $dbw |
1092 | * @param string $type Entity type |
1093 | * @return int |
1094 | */ |
1095 | private static function insertEntity( $dbw, $type ) { |
1096 | $dbw->newInsertQueryBuilder() |
1097 | ->insertInto( 'securepoll_entity' ) |
1098 | ->row( [ |
1099 | 'en_type' => $type, |
1100 | ] ) |
1101 | ->caller( __METHOD__ ) |
1102 | ->execute(); |
1103 | |
1104 | return $dbw->insertId(); |
1105 | } |
1106 | |
1107 | /** |
1108 | * Save properties and messages for an entity |
1109 | * |
1110 | * @param IDatabase $dbw |
1111 | * @param int $id |
1112 | * @param Entity $entity |
1113 | */ |
1114 | private static function savePropertiesAndMessages( $dbw, $id, $entity ) { |
1115 | $properties = []; |
1116 | foreach ( $entity->getAllProperties() as $key => $value ) { |
1117 | $properties[] = [ |
1118 | 'pr_entity' => $id, |
1119 | 'pr_key' => $key, |
1120 | 'pr_value' => $value, |
1121 | ]; |
1122 | } |
1123 | if ( $properties ) { |
1124 | $dbw->newReplaceQueryBuilder() |
1125 | ->replaceInto( 'securepoll_properties' ) |
1126 | ->uniqueIndexFields( [ 'pr_entity', 'pr_key' ] ) |
1127 | ->rows( $properties ) |
1128 | ->caller( __METHOD__ ) |
1129 | ->execute(); |
1130 | } |
1131 | |
1132 | $messages = []; |
1133 | $langs = $entity->getLangList(); |
1134 | foreach ( $entity->getMessageNames() as $name ) { |
1135 | foreach ( $langs as $lang ) { |
1136 | $value = $entity->getRawMessage( $name, $lang ); |
1137 | if ( $value !== false ) { |
1138 | $messages[] = [ |
1139 | 'msg_entity' => $id, |
1140 | 'msg_lang' => $lang, |
1141 | 'msg_key' => $name, |
1142 | 'msg_text' => $value, |
1143 | ]; |
1144 | } |
1145 | } |
1146 | } |
1147 | if ( $messages ) { |
1148 | $dbw->newReplaceQueryBuilder() |
1149 | ->replaceInto( 'securepoll_msgs' ) |
1150 | ->uniqueIndexFields( [ 'msg_entity', 'msg_lang', 'msg_key' ] ) |
1151 | ->rows( $messages ) |
1152 | ->caller( __METHOD__ ) |
1153 | ->execute(); |
1154 | } |
1155 | } |
1156 | |
1157 | /** |
1158 | * Combine form items for the class into the main array |
1159 | * |
1160 | * @param array &$outItems Array to insert the descriptors into |
1161 | * @param string $field Owning field name, for hide-if |
1162 | * @param string|array $types Type value(s) in the field, for hide-if |
1163 | * @param class-string|false $class Class with the ::getCreateDescriptors static method |
1164 | * @param string|null $category If given, ::getCreateDescriptors is |
1165 | * expected to return an array with subarrays for different categories |
1166 | * of descriptors, and this selects which subarray to process. |
1167 | * @param bool|null $disabled Should the field be disabled |
1168 | */ |
1169 | private static function processFormItems( |
1170 | &$outItems, $field, $types, $class, |
1171 | $category = null, |
1172 | $disabled = false |
1173 | ) { |
1174 | if ( $class === false ) { |
1175 | return; |
1176 | } |
1177 | |
1178 | $items = call_user_func_array( |
1179 | [ |
1180 | $class, |
1181 | 'getCreateDescriptors' |
1182 | ], |
1183 | [] |
1184 | ); |
1185 | |
1186 | if ( !is_array( $types ) ) { |
1187 | $types = [ $types ]; |
1188 | } |
1189 | |
1190 | if ( $category ) { |
1191 | if ( !isset( $items[$category] ) ) { |
1192 | return; |
1193 | } |
1194 | $items = $items[$category]; |
1195 | } |
1196 | |
1197 | foreach ( $items as $key => $item ) { |
1198 | if ( $disabled ) { |
1199 | $item['disabled'] = true; |
1200 | } |
1201 | if ( !isset( $outItems[$key] ) ) { |
1202 | if ( !isset( $item['hide-if'] ) ) { |
1203 | $item['hide-if'] = [ |
1204 | 'OR', |
1205 | [ 'AND' ] |
1206 | ]; |
1207 | } else { |
1208 | $item['hide-if'] = [ |
1209 | 'OR', |
1210 | [ 'AND' ], |
1211 | $item['hide-if'] |
1212 | ]; |
1213 | } |
1214 | $outItems[$key] = $item; |
1215 | } else { |
1216 | // @todo Detect if this is really the same descriptor? |
1217 | } |
1218 | foreach ( $types as $type ) { |
1219 | $outItems[$key]['hide-if'][1][] = [ |
1220 | '!==', |
1221 | $field, |
1222 | $type |
1223 | ]; |
1224 | } |
1225 | } |
1226 | } |
1227 | |
1228 | /** |
1229 | * Inject form field values for the class's properties and messages |
1230 | * |
1231 | * @param array &$formData Form data array |
1232 | * @param array $data Input data array |
1233 | * @param class-string|false $class Class with the ::getCreateDescriptors static method |
1234 | * @param string|null $category If given, ::getCreateDescriptors is |
1235 | * expected to return an array with subarrays for different categories |
1236 | * of descriptors, and this selects which subarray to process. |
1237 | */ |
1238 | private static function unprocessFormData( &$formData, $data, $class, $category ) { |
1239 | if ( $class === false ) { |
1240 | return; |
1241 | } |
1242 | |
1243 | $items = call_user_func_array( |
1244 | [ |
1245 | $class, |
1246 | 'getCreateDescriptors' |
1247 | ], |
1248 | [] |
1249 | ); |
1250 | |
1251 | if ( $category ) { |
1252 | if ( !isset( $items[$category] ) ) { |
1253 | return; |
1254 | } |
1255 | $items = $items[$category]; |
1256 | } |
1257 | |
1258 | foreach ( $items as $key => $item ) { |
1259 | if ( !isset( $item['SecurePoll_type'] ) ) { |
1260 | continue; |
1261 | } |
1262 | switch ( $item['SecurePoll_type'] ) { |
1263 | case 'property': |
1264 | if ( isset( $data['properties'][$key] ) ) { |
1265 | $formData[$key] = $data['properties'][$key]; |
1266 | } else { |
1267 | $formData[$key] = null; |
1268 | } |
1269 | break; |
1270 | case 'properties': |
1271 | $formData[$key] = []; |
1272 | foreach ( $data['properties'] as $k => $v ) { |
1273 | $formData[$key][$k] = $v; |
1274 | } |
1275 | break; |
1276 | case 'message': |
1277 | if ( isset( $data['messages'][$key] ) ) { |
1278 | $formData[$key] = $data['messages'][$key]; |
1279 | } else { |
1280 | $formData[$key] = null; |
1281 | } |
1282 | break; |
1283 | case 'messages': |
1284 | $formData[$key] = []; |
1285 | foreach ( $data['messages'] as $k => $v ) { |
1286 | $formData[$key][$k] = $v; |
1287 | } |
1288 | break; |
1289 | } |
1290 | } |
1291 | } |
1292 | |
1293 | /** |
1294 | * Check that the user is part of the electionadmin group |
1295 | * |
1296 | * @param string $value Username |
1297 | * @param array $alldata All form data |
1298 | * @param HTMLForm $containingForm Containing HTMLForm |
1299 | * @return bool|string true on success, string on error |
1300 | */ |
1301 | public function checkIfInElectionAdminUserGroup( $value, $alldata, HTMLForm $containingForm ) { |
1302 | $user = $this->userFactory->newFromName( $value ); |
1303 | if ( !$user || !in_array( 'electionadmin', $this->userGroupManager->getUserEffectiveGroups( $user ) ) ) { |
1304 | return $this->msg( |
1305 | 'securepoll-create-user-not-in-electionadmin-group', |
1306 | $value |
1307 | )->parse(); |
1308 | } |
1309 | |
1310 | return true; |
1311 | } |
1312 | |
1313 | public function checkElectionEndDate( $value, $formData ) { |
1314 | $startDate = new DateTime( $formData['election_startdate'], new DateTimeZone( 'GMT' ) ); |
1315 | $endDate = new DateTime( $value, new DateTimeZone( 'GMT' ) ); |
1316 | |
1317 | if ( $startDate >= $endDate ) { |
1318 | return $this->msg( 'securepoll-htmlform-daterange-end-before-start' )->parseAsBlock(); |
1319 | } |
1320 | |
1321 | return true; |
1322 | } |
1323 | |
1324 | /** |
1325 | * Check that a required field has been filled. |
1326 | * |
1327 | * This is a hack for using with cloner fields. Just setting required=true |
1328 | * breaks cloner fields when used with OOUI, in no-JS environments, because |
1329 | * the browser will prevent submission on clicking the remove button of an |
1330 | * empty field. |
1331 | * |
1332 | * @internal For use by the HTMLFormField |
1333 | * @param string $value |
1334 | * @return true|Message true on success, Message on error |
1335 | */ |
1336 | public static function checkRequired( $value ) { |
1337 | if ( $value === '' ) { |
1338 | return Status::newFatal( 'htmlform-required' )->getMessage(); |
1339 | } |
1340 | return true; |
1341 | } |
1342 | } |