Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.16% covered (danger)
35.16%
32 / 91
18.75% covered (danger)
18.75%
3 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Ballot
35.16% covered (danger)
35.16%
32 / 91
18.75% covered (danger)
18.75%
3 / 16
225.68
0.00% covered (danger)
0.00%
0 / 1
 getTallyTypes
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCreateDescriptors
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 getQuestionForm
n/a
0 / 0
n/a
0 / 0
0
 getMessageNames
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequest
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMessageLocalizer
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 msg
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserLang
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initRequest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 submitForm
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 submitQuestion
n/a
0 / 0
n/a
0 / 0
0
 unpackRecord
n/a
0 / 0
n/a
0 / 0
0
 convertRecord
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 convertScores
n/a
0 / 0
n/a
0 / 0
0
 factory
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getForm
92.31% covered (success)
92.31%
24 / 26
0.00% covered (danger)
0.00%
0 / 1
5.01
 setErrorStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 errorLocationIndicator
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 formatStatus
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Ballots;
4
5use InvalidArgumentException;
6use Language;
7use LogicException;
8use MediaWiki\Extension\SecurePoll\Context;
9use MediaWiki\Extension\SecurePoll\Entities\Election;
10use MediaWiki\Extension\SecurePoll\Entities\Entity;
11use MediaWiki\Extension\SecurePoll\Entities\Question;
12use MediaWiki\Request\WebRequest;
13use MediaWiki\Status\Status;
14use MessageLocalizer;
15
16/**
17 * Parent class for ballot forms. This is the UI component of a voting method.
18 */
19abstract class Ballot {
20    /** @var Election */
21    public $election;
22    /** @var Context */
23    public $context;
24    /** @var string|null */
25    public $currentVote;
26    /** @var true[]|null */
27    public $prevErrorIds;
28    /** @var true[]|null */
29    public $usedErrorIds;
30    /** @var BallotStatus|null */
31    public $prevStatus;
32    /** @var WebRequest|null */
33    private $request;
34    /** @var MessageLocalizer|null */
35    private $messageLocalizer;
36    /** @var Language|null */
37    private $userLang;
38
39    /** @var string[] */
40    public const BALLOT_TYPES = [
41        'approval' => ApprovalBallot::class,
42        'preferential' => PreferentialBallot::class,
43        'choose' => ChooseBallot::class,
44        'radio-range' => RadioRangeBallot::class,
45        'radio-range-comment' => RadioRangeCommentBallot::class,
46        'stv' => STVBallot::class,
47    ];
48
49    /**
50     * Get a list of names of tallying methods, which may be used to produce a
51     * result from this ballot type.
52     * @return array
53     */
54    public static function getTallyTypes() {
55        throw new LogicException( "Subclass must override ::getTallyTypes()" );
56    }
57
58    /**
59     * Return descriptors for any properties this type requires for poll
60     * creation, for the election, questions, and options.
61     *
62     * The returned array should have three keys, "election", "question", and
63     * "option", each mapping to an array of HTMLForm descriptors.
64     *
65     * The descriptors should have an additional key, "SecurePoll_type", with
66     * the value being "property" or "message".
67     *
68     * @return array
69     */
70    public static function getCreateDescriptors() {
71        return [
72            'election' => [
73                'shuffle-questions' => [
74                    'label-message' => 'securepoll-create-label-shuffle_questions',
75                    'type' => 'check',
76                    'hidelabel' => true,
77                    'SecurePoll_type' => 'property',
78                ],
79                'shuffle-options' => [
80                    'label-message' => 'securepoll-create-label-shuffle_options',
81                    'type' => 'check',
82                    'hidelabel' => true,
83                    'SecurePoll_type' => 'property',
84                ],
85            ],
86            'question' => [],
87            'option' => [],
88        ];
89    }
90
91    /**
92     * Get the HTML form segment for a single question
93     * @param Question $question
94     * @param array $options Array of options, in the order they should be displayed
95     * @return \OOUI\FieldsetLayout
96     */
97    abstract public function getQuestionForm( $question, $options );
98
99    /**
100     * Get any extra messages that this ballot type uses to render questions.
101     * Used to get the list of translatable messages for TranslatePage.
102     * @param Entity|null $entity
103     * @return array
104     * @see Election::getMessageNames()
105     */
106    public function getMessageNames( Entity $entity = null ) {
107        return [];
108    }
109
110    /**
111     * Get the request if it has been set, otherwise throw an exception.
112     *
113     * @return WebRequest
114     */
115    protected function getRequest(): WebRequest {
116        if ( !$this->request ) {
117            throw new LogicException(
118                'Ballot::initRequest() must be called before Ballot::getRequest()' );
119        }
120        return $this->request;
121    }
122
123    /**
124     * Get the MessageLocalizer if it has been set, otherwise throw an exception
125     *
126     * @return MessageLocalizer
127     */
128    private function getMessageLocalizer(): MessageLocalizer {
129        if ( !$this->messageLocalizer ) {
130            throw new LogicException(
131                'Ballot::initRequest() must be called before Ballot::getMessageLocalizer()' );
132        }
133        return $this->messageLocalizer;
134    }
135
136    /**
137     * Get a MediaWiki message. setMessageLocalizer() must have been called.
138     *
139     * This can be used instead of SecurePoll's native message system if the
140     * message does not vary depending on the election, and if there are no
141     * security concerns with allowing people who are not admins of the election
142     * to set the text.
143     *
144     * @param string $key
145     * @param mixed ...$params
146     * @return \Message
147     */
148    protected function msg( $key, ...$params ) {
149        return $this->getMessageLocalizer()->msg( $key, ...$params );
150    }
151
152    /**
153     * Get the user language, or throw an exception if it has not been set.
154     * @return Language
155     */
156    protected function getUserLang(): Language {
157        return $this->userLang;
158    }
159
160    /**
161     * Set request dependencies
162     *
163     * @param WebRequest $request
164     * @param MessageLocalizer $localizer
165     * @param Language $userLang
166     */
167    public function initRequest(
168        WebRequest $request,
169        MessageLocalizer $localizer,
170        Language $userLang
171    ) {
172        $this->request = $request;
173        $this->messageLocalizer = $localizer;
174        $this->userLang = $userLang;
175    }
176
177    /**
178     * Called when the form is submitted. This returns a Status object which,
179     * when successful, contains a voting record in the value member. To
180     * preserve voter privacy, voting records should be the same length
181     * regardless of voter choices.
182     * @return Status
183     */
184    public function submitForm() {
185        $questions = $this->election->getQuestions();
186        $record = '';
187        $status = new BallotStatus();
188
189        foreach ( $questions as $question ) {
190            $record .= $this->submitQuestion( $question, $status );
191        }
192        if ( $status->isOK() ) {
193            $status->value = $record;
194        }
195
196        return $status;
197    }
198
199    /**
200     * Construct a string record for a given question, during form submission.
201     *
202     * If there is a problem with the form data, the function should set a
203     * fatal error in the $status object and return null.
204     *
205     * @param Question $question
206     * @param BallotStatus $status
207     * @return string|null
208     */
209    abstract public function submitQuestion( $question, $status );
210
211    /**
212     * Unpack a string record into an array format suitable for the tally type
213     * @param string $record
214     * @return array|bool
215     */
216    abstract public function unpackRecord( $record );
217
218    /**
219     * Convert a record to a string of some kind
220     * @param string $record
221     * @param array $options
222     * @return string[]|false
223     */
224    public function convertRecord( $record, $options = [] ) {
225        $scores = $this->unpackRecord( $record );
226
227        return $this->convertScores( $scores );
228    }
229
230    /**
231     * Convert a score array to a string of some kind
232     * @param array $scores
233     * @param array $options
234     * @return string|string[]|false
235     */
236    abstract public function convertScores( $scores, $options = [] );
237
238    /**
239     * Create a ballot of the given type
240     * @param Context $context
241     * @param string $type
242     * @param Election $election
243     * @return Ballot
244     * @throws InvalidArgumentException
245     */
246    public static function factory( $context, $type, $election ) {
247        if ( !isset( self::BALLOT_TYPES[$type] ) ) {
248            throw new InvalidArgumentException( "Invalid ballot type: $type" );
249        }
250        $class = self::BALLOT_TYPES[$type];
251
252        return new $class( $context, $election );
253    }
254
255    /**
256     * Constructor.
257     * @param Context $context
258     * @param Election $election
259     */
260    public function __construct( $context, $election ) {
261        $this->context = $context;
262        $this->election = $election;
263    }
264
265    /**
266     * Get the HTML for this ballot. <form> tags should not be included,
267     * they will be added by the VotePage.
268     * @param bool|BallotStatus $prevStatus
269     * @return \OOUI\Element[]
270     */
271    public function getForm( $prevStatus = false ) {
272        $questions = $this->election->getQuestions();
273        if ( $this->election->getProperty( 'shuffle-questions' ) ) {
274            shuffle( $questions );
275        }
276        $shuffleOptions = $this->election->getProperty( 'shuffle-options' );
277        $this->setErrorStatus( $prevStatus );
278
279        $itemArray = [];
280        foreach ( $questions as $question ) {
281            $options = $question->getOptions();
282            if ( $shuffleOptions ) {
283                shuffle( $options );
284            }
285
286            $questionForm = $this->getQuestionForm(
287                    $question,
288                    $options
289            );
290            $questionForm->setLabel(
291                new \OOUI\HtmlSnippet( $question->parseMessage( 'text' ) )
292            );
293            $itemArray[] = $questionForm;
294        }
295        if ( $prevStatus ) {
296            $formStatus = new \OOUI\Element( [
297                'content' => new \OOUI\HTMLSnippet(
298                    $this->formatStatus( $prevStatus )
299                ),
300            ] );
301            array_unshift( $itemArray, $formStatus );
302        }
303
304        return $itemArray;
305    }
306
307    /**
308     * @param bool|BallotStatus $status
309     */
310    public function setErrorStatus( $status ) {
311        if ( $status ) {
312            $this->prevErrorIds = $status->getIds();
313            $this->prevStatus = $status;
314        } else {
315            $this->prevErrorIds = [];
316        }
317        $this->usedErrorIds = [];
318    }
319
320    public function errorLocationIndicator( $id ) {
321        if ( !isset( $this->prevErrorIds[$id] ) ) {
322            return '';
323        }
324        $this->usedErrorIds[$id] = true;
325
326        return new \OOUI\IconWidget( [
327            'icon' => 'error',
328            'title' => $this->prevStatus->spGetMessageText( $id ),
329            'id' => "$id-location",
330            'classes' => [ 'securepoll-error-location' ],
331            'flags' => 'error',
332         ] );
333    }
334
335    /**
336     * Convert a BallotStatus object to HTML
337     * @param BallotStatus $status
338     * @return string
339     */
340    public function formatStatus( $status ) {
341        return $status->spGetHTML( $this->usedErrorIds );
342    }
343}