Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
22.57% |
72 / 319 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
VotePage | |
22.57% |
72 / 319 |
|
0.00% |
0 / 12 |
2026.31 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
182 | |||
getTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showForm | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
20 | |||
getBallot | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
doSubmit | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
logVote | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
72 | |||
getSummaryOfVotes | |
85.71% |
72 / 84 |
|
0.00% |
0 / 1 |
21.17 | |||
getVoteDataFromRecord | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getQuestionMessage | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getOptionMessages | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
42 | |||
showJumpForm | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Pages; |
4 | |
5 | use ExtensionRegistry; |
6 | use HTMLForm; |
7 | use MediaWiki\Extension\SecurePoll\Ballots\Ballot; |
8 | use MediaWiki\Extension\SecurePoll\Entities\Election; |
9 | use MediaWiki\Extension\SecurePoll\Exceptions\InvalidDataException; |
10 | use MediaWiki\Extension\SecurePoll\Hooks\HookRunner; |
11 | use MediaWiki\Extension\SecurePoll\SpecialSecurePoll; |
12 | use MediaWiki\Extension\SecurePoll\User\Auth; |
13 | use MediaWiki\Extension\SecurePoll\User\RemoteMWAuth; |
14 | use MediaWiki\Extension\SecurePoll\User\Voter; |
15 | use MediaWiki\Extension\SecurePoll\VoteRecord; |
16 | use MediaWiki\HookContainer\HookContainer; |
17 | use MediaWiki\Html\Html; |
18 | use MediaWiki\Session\SessionManager; |
19 | use MediaWiki\Status\Status; |
20 | use MediaWiki\Title\Title; |
21 | use MediaWiki\User\User; |
22 | use MediaWiki\WikiMap\WikiMap; |
23 | use MobileContext; |
24 | use OOUI\MessageWidget; |
25 | use Wikimedia\IPUtils; |
26 | use Wikimedia\Rdbms\ILoadBalancer; |
27 | |
28 | /** |
29 | * The subpage for casting votes. |
30 | */ |
31 | class VotePage extends ActionPage { |
32 | /** @var Election|null */ |
33 | public $election; |
34 | /** @var Auth|null */ |
35 | public $auth; |
36 | /** @var User|null */ |
37 | public $user; |
38 | /** @var Voter|null */ |
39 | public $voter; |
40 | /** @var ILoadBalancer */ |
41 | private $loadBalancer; |
42 | /** @var HookRunner */ |
43 | private $hookRunner; |
44 | |
45 | /** |
46 | * @param SpecialSecurePoll $specialPage |
47 | * @param ILoadBalancer $loadBalancer |
48 | * @param HookContainer $hookContainer |
49 | */ |
50 | public function __construct( |
51 | SpecialSecurePoll $specialPage, |
52 | ILoadBalancer $loadBalancer, |
53 | HookContainer $hookContainer |
54 | ) { |
55 | parent::__construct( $specialPage ); |
56 | $this->loadBalancer = $loadBalancer; |
57 | $this->hookRunner = new HookRunner( $hookContainer ); |
58 | } |
59 | |
60 | /** |
61 | * Execute the subpage. |
62 | * @param array $params Array of subpage parameters. |
63 | */ |
64 | public function execute( $params ) { |
65 | $out = $this->specialPage->getOutput(); |
66 | $out->enableOOUI(); |
67 | $out->addJsConfigVars( 'SecurePollSubPage', 'vote' ); |
68 | $out->addModules( 'ext.securepoll.htmlform' ); |
69 | $out->addModuleStyles( [ |
70 | 'oojs-ui.styles.icons-alerts', |
71 | 'oojs-ui.styles.icons-movement' |
72 | ] ); |
73 | |
74 | if ( !count( $params ) ) { |
75 | $out->addWikiMsg( 'securepoll-too-few-params' ); |
76 | return; |
77 | } |
78 | |
79 | if ( preg_match( '/^[0-9]+$/', $params[0] ) ) { |
80 | $electionId = intval( $params[0] ); |
81 | $this->election = $this->context->getElection( $electionId ); |
82 | } else { |
83 | $electionId = str_replace( '_', ' ', $params[0] ); |
84 | $this->election = $this->context->getElectionByTitle( $electionId ); |
85 | } |
86 | |
87 | if ( !$this->election ) { |
88 | $out->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
89 | return; |
90 | } |
91 | |
92 | $this->auth = $this->election->getAuth(); |
93 | |
94 | // Get voter from session |
95 | $this->voter = $this->auth->getVoterFromSession( $this->election ); |
96 | |
97 | // If there's no session, try creating one. |
98 | // This will fail if the user is not authorized to vote in the election |
99 | if ( !$this->voter ) { |
100 | $status = $this->auth->newAutoSession( $this->election ); |
101 | if ( $status->isOK() ) { |
102 | $this->voter = $status->value; |
103 | } else { |
104 | $out->addWikiTextAsInterface( $status->getWikiText() ); |
105 | |
106 | return; |
107 | } |
108 | } |
109 | |
110 | $this->initLanguage( $this->voter, $this->election ); |
111 | $language = $this->getUserLang(); |
112 | $this->specialPage->getContext()->setLanguage( $language ); |
113 | |
114 | $out->setPageTitle( $this->election->getMessage( 'title' ) ); |
115 | |
116 | if ( !$this->election->isStarted() ) { |
117 | $out->addWikiMsg( |
118 | 'securepoll-not-started', |
119 | $language->timeanddate( $this->election->getStartDate() ), |
120 | $language->date( $this->election->getStartDate() ), |
121 | $language->time( $this->election->getStartDate() ) |
122 | ); |
123 | |
124 | return; |
125 | } |
126 | |
127 | if ( $this->election->isFinished() ) { |
128 | $out->addWikiMsg( |
129 | 'securepoll-finished', |
130 | $language->timeanddate( $this->election->getEndDate() ), |
131 | $language->date( $this->election->getEndDate() ), |
132 | $language->time( $this->election->getEndDate() ) |
133 | ); |
134 | |
135 | return; |
136 | } |
137 | |
138 | // Show jump form if necessary |
139 | if ( $this->election->getProperty( 'jump-url' ) ) { |
140 | $this->showJumpForm(); |
141 | |
142 | return; |
143 | } |
144 | |
145 | // This is when it starts getting all serious; disable JS |
146 | // that might be used to sniff cookies or log voting data. |
147 | $out->disallowUserJs(); |
148 | |
149 | // Show welcome |
150 | if ( $this->voter->isRemote() ) { |
151 | $out->addWikiMsg( 'securepoll-welcome', $this->voter->getName() ); |
152 | } |
153 | |
154 | // Show change notice |
155 | if ( $this->election->hasVoted( $this->voter ) && !$this->election->allowChange() ) { |
156 | $out->addWikiMsg( 'securepoll-change-disallowed' ); |
157 | |
158 | return; |
159 | } |
160 | |
161 | // Show/submit the form |
162 | if ( $this->specialPage->getRequest()->wasPosted() ) { |
163 | $this->doSubmit(); |
164 | } else { |
165 | $this->showForm(); |
166 | } |
167 | } |
168 | |
169 | /** |
170 | * @return Title |
171 | */ |
172 | public function getTitle() { |
173 | return $this->specialPage->getPageTitle( 'vote/' . $this->election->getId() ); |
174 | } |
175 | |
176 | /** |
177 | * Show the voting form. |
178 | * @param Status|false $status |
179 | */ |
180 | public function showForm( $status = false ) { |
181 | $out = $this->specialPage->getOutput(); |
182 | |
183 | // Show introduction |
184 | if ( $this->election->hasVoted( $this->voter ) && $this->election->allowChange() ) { |
185 | $out->addWikiMsg( 'securepoll-change-allowed' ); |
186 | } |
187 | $out->addWikiTextAsInterface( $this->election->getMessage( 'intro' ) ); |
188 | |
189 | // Show form |
190 | $form = new \OOUI\FormLayout( [ |
191 | 'action' => $this->getTitle()->getLocalURL( "action=vote" ), |
192 | 'method' => 'post', |
193 | 'items' => $this->getBallot()->getForm( $status ) |
194 | ] ); |
195 | |
196 | // Show comments section |
197 | if ( $this->election->getProperty( 'request-comment' ) ) { |
198 | $form->addItems( [ |
199 | new \OOUI\FieldsetLayout( [ |
200 | 'label' => $this->msg( 'securepoll-header-comments' ), |
201 | 'items' => [ |
202 | new \OOUI\FieldLayout( |
203 | new \OOUI\MultilineTextInputWidget( [ |
204 | 'name' => 'securepoll_comment', |
205 | 'rows' => 3, |
206 | // vote_record is a BLOB, so this can't be infinity |
207 | 'maxLength' => 10000, |
208 | ] ), |
209 | [ |
210 | 'label' => new \OOUI\HtmlSnippet( |
211 | $this->election->parseMessage( 'comment-prompt' ) |
212 | ), |
213 | 'align' => 'top' |
214 | ] |
215 | ) |
216 | ] |
217 | ] ) |
218 | ] ); |
219 | } |
220 | |
221 | $form->addItems( [ |
222 | new \OOUI\FieldLayout( |
223 | new \OOUI\ButtonInputWidget( [ |
224 | 'label' => $this->msg( 'securepoll-submit' )->text(), |
225 | 'flags' => [ 'primary', 'progressive' ], |
226 | 'type' => 'submit', |
227 | ] ) |
228 | ), |
229 | new \OOUI\HiddenInputWidget( [ |
230 | 'name' => 'edit_token', |
231 | 'value' => SessionManager::getGlobalSession()->getToken()->toString(), |
232 | ] ) |
233 | ] ); |
234 | |
235 | $out->addHTML( $form ); |
236 | } |
237 | |
238 | /** |
239 | * Get the Ballot for this election, with injected request dependencies. |
240 | * @return Ballot |
241 | */ |
242 | private function getBallot() { |
243 | $ballot = $this->election->getBallot(); |
244 | $ballot->initRequest( |
245 | $this->specialPage->getRequest(), |
246 | $this->specialPage, |
247 | $this->getUserLang() |
248 | ); |
249 | return $ballot; |
250 | } |
251 | |
252 | /** |
253 | * Submit the voting form. If successful, adds a record to the database. |
254 | * Shows an error message on failure. |
255 | */ |
256 | public function doSubmit() { |
257 | $ballot = $this->getBallot(); |
258 | $status = $ballot->submitForm(); |
259 | if ( !$status->isOK() ) { |
260 | $this->showForm( $status ); |
261 | } else { |
262 | $voteRecord = VoteRecord::newFromBallotData( |
263 | $status->value, |
264 | $this->specialPage->getRequest()->getText( 'securepoll_comment' ) |
265 | ); |
266 | $this->logVote( $voteRecord->getBlob() ); |
267 | } |
268 | } |
269 | |
270 | /** |
271 | * Add a vote to the database with the given unencrypted answer record. |
272 | * @param string $record |
273 | */ |
274 | public function logVote( $record ) { |
275 | $out = $this->specialPage->getOutput(); |
276 | $request = $this->specialPage->getRequest(); |
277 | |
278 | $now = wfTimestampNow(); |
279 | |
280 | $crypt = $this->election->getCrypt(); |
281 | if ( !$crypt ) { |
282 | $encrypted = $record; |
283 | } else { |
284 | $status = $crypt->encrypt( $record ); |
285 | if ( !$status->isOK() ) { |
286 | $out->addWikiTextAsInterface( $status->getWikiText( 'securepoll-encrypt-error' ) ); |
287 | |
288 | return; |
289 | } |
290 | $encrypted = $status->value; |
291 | } |
292 | |
293 | $dbw = $this->loadBalancer->getConnection( ILoadBalancer::DB_PRIMARY ); |
294 | $dbw->startAtomic( __METHOD__ ); |
295 | |
296 | // Mark previous votes as old |
297 | $dbw->newUpdateQueryBuilder() |
298 | ->update( 'securepoll_votes' ) |
299 | ->set( [ 'vote_current' => 0 ] ) |
300 | ->where( [ |
301 | 'vote_election' => $this->election->getId(), |
302 | 'vote_voter' => $this->voter->getId(), |
303 | ] ) |
304 | ->caller( __METHOD__ ) |
305 | ->execute(); |
306 | |
307 | // Add vote to log |
308 | $xff = ''; |
309 | if ( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { |
310 | $xff = $_SERVER['HTTP_X_FORWARDED_FOR']; |
311 | } |
312 | |
313 | $token = SessionManager::getGlobalSession()->getToken(); |
314 | $tokenMatch = $token->match( $request->getVal( 'edit_token' ) ); |
315 | |
316 | $dbw->newInsertQueryBuilder() |
317 | ->insertInto( 'securepoll_votes' ) |
318 | ->row( [ |
319 | 'vote_election' => $this->election->getId(), |
320 | 'vote_voter' => $this->voter->getId(), |
321 | 'vote_voter_name' => $this->voter->getName(), |
322 | 'vote_voter_domain' => $this->voter->getDomain(), |
323 | 'vote_record' => $encrypted, |
324 | 'vote_ip' => IPUtils::toHex( $request->getIP() ), |
325 | 'vote_xff' => $xff, |
326 | 'vote_ua' => $_SERVER['HTTP_USER_AGENT'], |
327 | 'vote_timestamp' => $now, |
328 | 'vote_current' => 1, |
329 | 'vote_token_match' => $tokenMatch ? 1 : 0, |
330 | 'vote_struck' => 0, |
331 | 'vote_cookie_dup' => 0, |
332 | ] ) |
333 | ->caller( __METHOD__ ) |
334 | ->execute(); |
335 | $voteId = $dbw->insertId(); |
336 | $dbw->endAtomic( __METHOD__ ); |
337 | |
338 | $votingData = $this->getVoteDataFromRecord( $record ); |
339 | $languageCode = $this->specialPage->getContext()->getLanguage()->getCode(); |
340 | $summary = $this->getSummaryOfVotes( $votingData, $languageCode ); |
341 | $out->addHtml( $summary ); |
342 | |
343 | if ( $crypt ) { |
344 | $receipt = sprintf( "SPID: %10d\n%s", $voteId, $encrypted ); |
345 | $out->addWikiMsg( 'securepoll-gpg-receipt', $receipt ); |
346 | } |
347 | |
348 | $returnUrl = $this->election->getProperty( 'return-url' ); |
349 | $returnText = $this->election->getMessage( 'return-text' ); |
350 | if ( $returnUrl ) { |
351 | if ( strval( $returnText ) === '' ) { |
352 | $returnText = $returnUrl; |
353 | } |
354 | $link = "[$returnUrl $returnText]"; |
355 | $out->addWikiMsg( 'securepoll-return', $link ); |
356 | } |
357 | } |
358 | |
359 | /** |
360 | * Get summary of voting in user readable version |
361 | * |
362 | * @param array $votingData |
363 | * @param string $languageCode |
364 | * @return string |
365 | */ |
366 | public function getSummaryOfVotes( $votingData, $languageCode ) { |
367 | $data = $votingData['votes']; |
368 | $comment = $votingData['comment']; |
369 | |
370 | /** |
371 | * if record cannot be unpacked correctly, show error |
372 | */ |
373 | if ( !$data ) { |
374 | return new MessageWidget( [ |
375 | 'type' => 'error', |
376 | 'label' => $this->msg( 'securepoll-vote-result-error-label' ) |
377 | ] ); |
378 | } |
379 | |
380 | $summary = new MessageWidget( [ |
381 | 'type' => 'success', |
382 | 'label' => $this->msg( 'securepoll-thanks' ) |
383 | ] ); |
384 | |
385 | $summary .= Html::element( 'h2', [ 'class' => 'securepoll-vote-result-heading' ], |
386 | $this->msg( 'securepoll-vote-result-intro-label' ) ); |
387 | |
388 | foreach ( $data as $questionIndex => $votes ) { |
389 | $questionMsg = $this->getQuestionMessage( $languageCode, $questionIndex ); |
390 | $optionsMsgs = $this->getOptionMessages( $languageCode, $votes ); |
391 | if ( !isset( $questionMsg[$questionIndex]['text'] ) ) { |
392 | continue; |
393 | } |
394 | $questionText = $questionMsg[$questionIndex]['text']; |
395 | $html = Html::openElement( 'div', [ 'class' => 'securepoll-vote-result-question-cnt' ] ); |
396 | $html .= Html::element( |
397 | 'p', [ 'class' => 'securepoll-vote-result-question' ], |
398 | $this->msg( 'securepoll-vote-result-question-label', $questionText ) |
399 | ); |
400 | |
401 | $listType = 'ul'; |
402 | $votedItems = []; |
403 | if ( $this->election->getTallyType() === 'droop-quota' ) { |
404 | $listType = 'ol'; |
405 | foreach ( $optionsMsgs as $option ) { |
406 | $votedItems[] = Html::rawElement( 'li', [], $option['text'] ); |
407 | } |
408 | } else { |
409 | $notVotedItems = []; |
410 | foreach ( $optionsMsgs as $optionIndex => $option ) { |
411 | $optionText = $optionsMsgs[$optionIndex]['text']; |
412 | $count = $optionIndex; |
413 | if ( isset( $votes[ $optionIndex ] ) ) { |
414 | $count = $votes[ $optionIndex ]; |
415 | } |
416 | |
417 | if ( $this->election->getTallyType() === 'plurality' || |
418 | $this->election->getTallyType() === 'histogram-range' ) { |
419 | if ( isset( $questionMsg[$questionIndex]['column' . $count ] ) ) { |
420 | $columnLabel = $questionMsg[$questionIndex]['column' . $count ]; |
421 | $votedItems[] = Html::element( 'li', [], |
422 | $this->msg( 'securepoll-vote-result-voted-option-label', $optionText, $columnLabel ) |
423 | ); |
424 | continue; |
425 | } |
426 | if ( is_int( $count ) && $count > 0 ) { |
427 | $positiveCount = '+' . $count; |
428 | if ( isset( $questionMsg[$questionIndex]['column' . $positiveCount ] ) ) { |
429 | $columnLabel = $questionMsg[$questionIndex]['column' . $positiveCount ]; |
430 | $votedItems[] = Html::element( 'li', [], |
431 | $this->msg( 'securepoll-vote-result-voted-option-label', $optionText, $columnLabel ) |
432 | ); |
433 | continue; |
434 | } |
435 | } |
436 | } |
437 | |
438 | if ( $this->election->getTallyType() === 'schulze' && $count === 1000 ) { |
439 | $notVotedItems[] = Html::element( 'li', [], |
440 | $this->msg( 'securepoll-vote-result-not-voted-option-label', $optionText ) |
441 | ); |
442 | continue; |
443 | } |
444 | |
445 | if ( $count === 0 ) { |
446 | $notVotedItems[] = Html::element( 'li', [], |
447 | $this->msg( 'securepoll-vote-result-not-checked-option-label', $optionText ) |
448 | ); |
449 | continue; |
450 | } |
451 | if ( $this->election->getTallyType() === 'plurality' ) { |
452 | $votedItems[] = Html::element( 'li', [], |
453 | $this->msg( 'securepoll-vote-result-checked-option-label', $optionText ) |
454 | ); |
455 | continue; |
456 | } |
457 | $votedItems[] = Html::element( 'li', [], |
458 | $this->msg( 'securepoll-vote-result-rated-option-label', $optionText, $count ) |
459 | ); |
460 | } |
461 | |
462 | if ( $notVotedItems !== [] ) { |
463 | $votedItems[] = Html::rawElement( 'ul', [ 'class' => 'securepoll-vote-result-no-vote' ], |
464 | implode( "\n", $notVotedItems ) |
465 | ); |
466 | } |
467 | } |
468 | |
469 | $html .= Html::rawElement( $listType, [ 'class' => 'securepoll-vote-result-options' ], |
470 | implode( "\n", $votedItems ) |
471 | ); |
472 | $html .= Html::closeElement( 'div' ); |
473 | $summary .= $html; |
474 | } |
475 | |
476 | if ( $comment !== '' ) { |
477 | $summary .= Html::element( 'div', [ 'class' => 'securepoll-vote-result-comment' ], |
478 | $this->msg( 'securepoll-vote-result-comment', $comment )->plain() |
479 | ); |
480 | } |
481 | return $summary; |
482 | } |
483 | |
484 | /** |
485 | * @param string $record |
486 | * @return array |
487 | */ |
488 | public function getVoteDataFromRecord( $record ) { |
489 | $blob = VoteRecord::readBlob( $record ); |
490 | $ballotData = $blob->value->getBallotData(); |
491 | $data = []; |
492 | $data['votes'] = $this->getBallot()->unpackRecord( $ballotData ); |
493 | $data['comment'] = $blob->value->getComment(); |
494 | return $data; |
495 | } |
496 | |
497 | /** |
498 | * @param string $languageCode |
499 | * @param int $questionIndex |
500 | * @return string[][] |
501 | */ |
502 | private function getQuestionMessage( $languageCode, $questionIndex ) { |
503 | $questionMsg = $this->context->getMessages( $languageCode, [ $questionIndex ] ); |
504 | if ( !$questionMsg ) { |
505 | $fallbackLangCode = $this->election->getLanguage(); |
506 | $questionMsg = $this->context->getMessages( $fallbackLangCode, [ $questionIndex ] ); |
507 | } |
508 | return $questionMsg; |
509 | } |
510 | |
511 | /** |
512 | * @param string $languageCode |
513 | * @param array $votes |
514 | * @return string[][] |
515 | */ |
516 | private function getOptionMessages( $languageCode, $votes ) { |
517 | $optionsMsgs = $this->context->getMessages( $languageCode, $votes ); |
518 | if ( !$optionsMsgs || count( $votes ) !== count( $optionsMsgs ) ) { |
519 | $languageCode = $this->election->getLanguage(); |
520 | $optionsMsgs = $this->context->getMessages( $languageCode, $votes ); |
521 | } |
522 | if ( !$optionsMsgs || count( $votes ) !== count( $optionsMsgs ) ) { |
523 | $msgsKeys = []; |
524 | foreach ( $votes as $questionKey => $item ) { |
525 | $msgsKeys[] = $questionKey; |
526 | } |
527 | $optionsMsgs = $this->context->getMessages( $languageCode, $msgsKeys ); |
528 | } |
529 | return $optionsMsgs; |
530 | } |
531 | |
532 | /** |
533 | * Show a page informing the user that they must go to another wiki to |
534 | * cast their vote, and a button which takes them there. |
535 | * |
536 | * Clicking the button transmits a hash of their auth token, so that the |
537 | * remote server can authenticate them. |
538 | */ |
539 | public function showJumpForm() { |
540 | $user = $this->specialPage->getUser(); |
541 | $out = $this->specialPage->getOutput(); |
542 | |
543 | $url = $this->election->getProperty( 'jump-url' ); |
544 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { |
545 | $mobileUrl = $this->election->getProperty( 'mobile-jump-url' ); |
546 | // @phan-suppress-next-line PhanUndeclaredClassMethod |
547 | $mobileContext = MobileContext::singleton(); |
548 | if ( $mobileUrl && $mobileContext->usingMobileDomain() ) { |
549 | $url = $mobileUrl; |
550 | } |
551 | } |
552 | if ( !$url ) { |
553 | throw new InvalidDataException( 'Configuration error: no jump-url' ); |
554 | } |
555 | |
556 | $id = $this->election->getProperty( 'jump-id' ); |
557 | if ( !$id ) { |
558 | throw new InvalidDataException( 'Configuration error: no jump-id' ); |
559 | } |
560 | $url .= "/login/$id"; |
561 | |
562 | $this->hookRunner->onSecurePoll_JumpUrl( $this, $url ); |
563 | |
564 | $out->addWikiTextAsInterface( $this->election->getMessage( 'jump-text' ) ); |
565 | $hiddenFields = [ |
566 | 'token' => RemoteMWAuth::encodeToken( $user->getToken() ), |
567 | 'id' => $user->getId(), |
568 | 'wiki' => WikiMap::getCurrentWikiId(), |
569 | ]; |
570 | |
571 | $htmlForm = HTMLForm::factory( |
572 | 'ooui', |
573 | [], |
574 | $this->specialPage->getContext() |
575 | )->setSubmitTextMsg( 'securepoll-jump' )->setAction( $url )->addHiddenFields( |
576 | $hiddenFields |
577 | )->prepareForm(); |
578 | |
579 | $out->addHTML( $htmlForm->getHTML( false ) ); |
580 | } |
581 | } |