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