Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 99 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
ElectionTallier | |
0.00% |
0 / 99 |
|
0.00% |
0 / 8 |
1260 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
setupTalliers | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
execute | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
addRecord | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
getJSONResult | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
loadJSONResult | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
getHtmlResult | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
getTextResult | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Talliers; |
4 | |
5 | use MediaWiki\Extension\SecurePoll\Ballots\Ballot; |
6 | use MediaWiki\Extension\SecurePoll\Context; |
7 | use MediaWiki\Extension\SecurePoll\Crypt\Crypt; |
8 | use MediaWiki\Extension\SecurePoll\Entities\Election; |
9 | use MediaWiki\Extension\SecurePoll\Entities\Question; |
10 | use MediaWiki\Extension\SecurePoll\Exceptions\InvalidDataException; |
11 | use MediaWiki\Extension\SecurePoll\Store\Store; |
12 | use MediaWiki\Extension\SecurePoll\VoteRecord; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\Logger\LoggerFactory; |
15 | use MediaWiki\Status\Status; |
16 | |
17 | /** |
18 | * A helper class for tallying a whole election (with multiple questions). |
19 | * Most of the functionality is contained in the Tallier subclasses |
20 | * which operate on a single question at a time. |
21 | * |
22 | * A convenience function for accessing this class is |
23 | * Election::tally(). |
24 | */ |
25 | class ElectionTallier { |
26 | /** @var Ballot|null */ |
27 | public $ballot; |
28 | /** @var Context */ |
29 | public $context; |
30 | /** @var Crypt|false */ |
31 | public $crypt; |
32 | /** @var Election */ |
33 | public $election; |
34 | /** @var Question[]|null */ |
35 | public $questions; |
36 | /** @var Tallier[] */ |
37 | public $talliers = []; |
38 | /** @var string[] */ |
39 | public $comments = []; |
40 | |
41 | /** |
42 | * Constructor. |
43 | * @param Context $context |
44 | * @param Election $election |
45 | */ |
46 | public function __construct( $context, $election ) { |
47 | $this->context = $context; |
48 | $this->election = $election; |
49 | } |
50 | |
51 | /** |
52 | * Set up a Tallier of the appropriate type for every question |
53 | * @throws InvalidDataException |
54 | */ |
55 | protected function setupTalliers() { |
56 | $questions = $this->election->getQuestions(); |
57 | $this->talliers = []; |
58 | $tallyType = $this->election->getTallyType(); |
59 | foreach ( $questions as $question ) { |
60 | $tallier = $this->context->newTallier( $tallyType, $this, $question ); |
61 | if ( !$tallier ) { |
62 | throw new InvalidDataException( 'Invalid tally type' ); |
63 | } |
64 | $this->talliers[$question->getId()] = $tallier; |
65 | } |
66 | } |
67 | |
68 | /** |
69 | * Do the tally. Returns a Status object. On success, the value property |
70 | * of the status will be an array of Tallier objects, which can |
71 | * be queried for results information. |
72 | * @return Status |
73 | */ |
74 | public function execute() { |
75 | $store = $this->context->getStore(); |
76 | $this->crypt = $this->election->getCrypt(); |
77 | $this->ballot = $this->election->getBallot(); |
78 | $this->setupTalliers(); |
79 | |
80 | // T288366 Tallies fail on beta/prod with little visibility |
81 | // Add logging to gain more context into where it fails |
82 | LoggerFactory::getInstance( 'AdHocDebug' )->info( |
83 | 'Starting queued election tally', |
84 | [ |
85 | 'electionId' => $this->election->getId(), |
86 | ] |
87 | ); |
88 | |
89 | $status = $store->callbackValidVotes( |
90 | $this->election->getId(), |
91 | [ |
92 | $this, |
93 | 'addRecord' |
94 | ] |
95 | ); |
96 | |
97 | if ( $this->crypt ) { |
98 | // Delete temporary files |
99 | $this->crypt->cleanup(); |
100 | } |
101 | |
102 | if ( !$status->isOK() ) { |
103 | return $status; |
104 | } |
105 | |
106 | foreach ( $this->talliers as $tallier ) { |
107 | $tallier->finishTally(); |
108 | } |
109 | |
110 | return Status::newGood( $this->talliers ); |
111 | } |
112 | |
113 | /** |
114 | * Add a record. This is the callback function for Store::callbackValidVotes(). |
115 | * On error, the Status object returned here will be passed through back to |
116 | * the caller of callbackValidVotes(). |
117 | * |
118 | * @param Store $store |
119 | * @param string $record Encrypted, packed record. |
120 | * @return Status |
121 | */ |
122 | public function addRecord( $store, $record ) { |
123 | # Decrypt and unpack |
124 | if ( $this->crypt ) { |
125 | $status = $this->crypt->decrypt( $record ); |
126 | if ( !$status->isOK() ) { |
127 | return $status; |
128 | } |
129 | $record = $status->value; |
130 | } |
131 | $status = VoteRecord::readBlob( $record ); |
132 | if ( !$status->isOK() ) { |
133 | return $status; |
134 | } |
135 | /** @var VoteRecord $voteRecord */ |
136 | $voteRecord = $status->value; |
137 | $scores = $this->ballot->unpackRecord( $voteRecord->getBallotData() ); |
138 | |
139 | # Add the record to the underlying question-specific tallier objects |
140 | foreach ( $this->election->getQuestions() as $question ) { |
141 | $qid = $question->getId(); |
142 | if ( !isset( $scores[$qid] ) ) { |
143 | return Status::newFatal( 'securepoll-tally-error' ); |
144 | } |
145 | if ( !$this->talliers[$qid]->addVote( $scores[$qid] ) ) { |
146 | return Status::newFatal( 'securepoll-tally-error' ); |
147 | } |
148 | } |
149 | |
150 | if ( $voteRecord->getComment() !== '' ) { |
151 | $this->comments[] = $voteRecord->getComment(); |
152 | } |
153 | |
154 | return Status::newGood(); |
155 | } |
156 | |
157 | /** |
158 | * Get a simple array structure representing results for this tally. Should |
159 | * only be called after execute(). |
160 | * @return array |
161 | */ |
162 | public function getJSONResult() { |
163 | $data = [ |
164 | 'type' => $this->election->getTallyType(), |
165 | 'results' => [], |
166 | ]; |
167 | foreach ( $this->election->getQuestions() as $question ) { |
168 | $data['results'][ $question->getId() ] = $this->talliers[ $question->getId() ]->getJSONResult(); |
169 | } |
170 | if ( $this->comments ) { |
171 | $data['comments'] = $this->comments; |
172 | } |
173 | return $data; |
174 | } |
175 | |
176 | /** |
177 | * Restores results from getJSONResult |
178 | * @param array{results:array} $data |
179 | */ |
180 | public function loadJSONResult( $data ) { |
181 | $this->setupTalliers(); |
182 | foreach ( $data['results'] as $questionid => $questiondata ) { |
183 | $this->talliers[$questionid]->loadJSONResult( $questiondata ); |
184 | } |
185 | foreach ( ( $data['comments'] ?? [] ) as $comment ) { |
186 | if ( is_string( $comment ) ) { |
187 | $this->comments[] = $comment; |
188 | } |
189 | } |
190 | } |
191 | |
192 | /** |
193 | * Get HTML formatted results for this tally. Should only be called after |
194 | * execute(). |
195 | * @return string |
196 | */ |
197 | public function getHtmlResult() { |
198 | $s = ''; |
199 | foreach ( $this->election->getQuestions() as $question ) { |
200 | if ( $s !== '' ) { |
201 | $s .= "<hr/>\n"; |
202 | } |
203 | $tallier = $this->talliers[$question->getId()]; |
204 | $s .= $tallier->getHtmlResult(); |
205 | } |
206 | |
207 | if ( count( $this->comments ) ) { |
208 | $s .= Html::element( 'h2', [], |
209 | wfMessage( 'securepoll-tally-comments' )->text() ) . "\n"; |
210 | $first = true; |
211 | foreach ( $this->comments as $comment ) { |
212 | if ( $first ) { |
213 | $first = false; |
214 | } else { |
215 | $s .= "<hr/>\n"; |
216 | } |
217 | $s .= nl2br( htmlspecialchars( $comment ) ) . "\n"; |
218 | } |
219 | } |
220 | |
221 | return $s; |
222 | } |
223 | |
224 | /** |
225 | * Get text formatted results for this tally. Should only be called after |
226 | * execute(). |
227 | * @return string |
228 | */ |
229 | public function getTextResult() { |
230 | $s = ''; |
231 | foreach ( $this->election->getQuestions() as $question ) { |
232 | if ( $s !== '' ) { |
233 | $s .= "\n"; |
234 | } |
235 | $tallier = $this->talliers[$question->getId()]; |
236 | $s .= $tallier->getTextResult(); |
237 | } |
238 | |
239 | if ( count( $this->comments ) ) { |
240 | $s .= "\n\n" . wfMessage( 'securepoll-tally-comments' )->text() . "\n\n"; |
241 | $first = true; |
242 | foreach ( $this->comments as $comment ) { |
243 | if ( $first ) { |
244 | $first = false; |
245 | } else { |
246 | $s .= "\n" . str_repeat( '-', 80 ) . "\n"; |
247 | } |
248 | $s .= $comment; |
249 | } |
250 | } |
251 | |
252 | return $s; |
253 | } |
254 | } |