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