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