Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
36.02% |
85 / 236 |
|
12.00% |
3 / 25 |
CRAP | |
0.00% |
0 / 1 |
Election | |
36.02% |
85 / 236 |
|
12.00% |
3 / 25 |
1508.36 | |
0.00% |
0 / 1 |
__construct | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
getMessageNames | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
getElection | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getChildren | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStartDate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getEndDate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isStarted | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
isFinished | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getVotesCount | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getBallot | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getQualifiedStatus | |
100.00% |
68 / 68 |
|
100.00% |
1 / 1 |
30 | |||
isAdmin | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
hasVoted | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
allowChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQuestions | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getAuth | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getLanguage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCrypt | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getTallyType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
dumpVotesToCallback | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
getConfXml | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
12 | |||
getPropertyDumpExclusion | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
tally | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getTallyFromDb | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
getTallyResultTimeFromDb | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\SecurePoll\Entities; |
4 | |
5 | use InvalidArgumentException; |
6 | use MediaWiki\Context\RequestContext; |
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\Exceptions\InvalidDataException; |
11 | use MediaWiki\Extension\SecurePoll\Talliers\ElectionTallier; |
12 | use MediaWiki\Extension\SecurePoll\User\Auth; |
13 | use MediaWiki\Extension\SecurePoll\User\Voter; |
14 | use MediaWiki\Permissions\Authority; |
15 | use MediaWiki\Status\Status; |
16 | use MediaWiki\Xml\Xml; |
17 | use Wikimedia\Rdbms\IDatabase; |
18 | |
19 | /** |
20 | * Class representing an *election*. The term is intended to include straw polls, |
21 | * surveys, etc. An election has one or more *questions* which voters answer. |
22 | * The *voters* submit their *votes*, which are later tallied to provide a result. |
23 | * An election runs only once and produces a single result. |
24 | * |
25 | * Each election has its own independent set of voters. Voters are created |
26 | * when the underlying user attempts to vote. A voter may vote more than once, |
27 | * unless the election disallows this, but only one of their votes is counted. |
28 | * |
29 | * Elections have a list of key/value pairs called properties, which are defined |
30 | * and used by various modules in order to configure the election. The properties, |
31 | * in order of the module that defines them, are as follows: |
32 | * |
33 | * Election |
34 | * min-edits |
35 | * Minimum number of edits needed to be qualified |
36 | * max-registration |
37 | * Latest acceptable registration date |
38 | * not-sitewide-blocked |
39 | * True if voters need to not have a sitewide block |
40 | * not-partial-blocked |
41 | * True if voters need to not have a partial block |
42 | * not-bot |
43 | * True if voters need to not have the bot permission |
44 | * need-group |
45 | * The name of an MW group voters need to be in |
46 | * need-list |
47 | * The name of a SecurePoll list voters need to be in |
48 | * need-central-list |
49 | * The name of a list in the CentralAuth database which is linked |
50 | * to globaluser.gu_id |
51 | * include-list |
52 | * The name of a SecurePoll list of voters who can vote regardless of the above |
53 | * exclude-list |
54 | * The name of a SecurePoll list of voters who may not vote regardless of the above |
55 | * admins |
56 | * A list of admin names, pipe separated |
57 | * disallow-change |
58 | * True if a voter is not allowed to change their vote |
59 | * encrypt-type |
60 | * The encryption module name |
61 | * not-centrally-blocked |
62 | * True if voters need to not be blocked on more than X projects |
63 | * central-block-threshold |
64 | * Number of blocks across projects that disqualify a user from voting. |
65 | * voter-privacy |
66 | * True to disable transparency features (public voter list and |
67 | * public encrypted record dump) in favour of preserving voter |
68 | * privacy. |
69 | * |
70 | * See the other module for documentation of the following. |
71 | * |
72 | * RemoteMWAuth |
73 | * remote-mw-script-path |
74 | * |
75 | * Ballot |
76 | * shuffle-questions |
77 | * shuffle-options |
78 | * |
79 | * OpenSslCrypt |
80 | * openssl-encrypt-key |
81 | * openssl-sign-key |
82 | * openssl-decrypt-key |
83 | * openssl-verify-key |
84 | * |
85 | * VotePage |
86 | * jump-url |
87 | * jump-id |
88 | * return-url |
89 | */ |
90 | class Election extends Entity { |
91 | /** @var Question[]|null */ |
92 | public $questions; |
93 | /** @var Auth|null */ |
94 | public $auth; |
95 | /** @var Ballot|null */ |
96 | public $ballot; |
97 | /** @var string */ |
98 | public $id; |
99 | /** @var string */ |
100 | public $title; |
101 | /** @var string */ |
102 | public $ballotType; |
103 | /** @var string */ |
104 | public $tallyType; |
105 | /** @var string */ |
106 | public $primaryLang; |
107 | /** @var string */ |
108 | public $startDate; |
109 | /** @var string */ |
110 | public $endDate; |
111 | /** @var string */ |
112 | public $authType; |
113 | /** @var int */ |
114 | public $owner = 0; |
115 | |
116 | /** |
117 | * Constructor. |
118 | * |
119 | * Do not use this constructor directly, instead use |
120 | * Context::getElection(). |
121 | * @param Context $context |
122 | * @param array $info |
123 | */ |
124 | public function __construct( $context, $info ) { |
125 | parent::__construct( $context, 'election', $info ); |
126 | $this->id = $info['id']; |
127 | $this->title = $info['title']; |
128 | $this->ballotType = $info['ballot']; |
129 | $this->tallyType = $info['tally']; |
130 | $this->primaryLang = $info['primaryLang']; |
131 | $this->startDate = $info['startDate']; |
132 | $this->endDate = $info['endDate']; |
133 | $this->authType = $info['auth']; |
134 | if ( isset( $info['owner'] ) ) { |
135 | $this->owner = $info['owner']; |
136 | } |
137 | } |
138 | |
139 | /** |
140 | * Get a list of localisable message names. See Entity. |
141 | * @return array |
142 | */ |
143 | public function getMessageNames() { |
144 | return [ |
145 | 'title', |
146 | 'intro', |
147 | 'jump-text', |
148 | 'return-text', |
149 | 'unqualified-error', |
150 | 'comment-prompt' |
151 | ]; |
152 | } |
153 | |
154 | /** |
155 | * Get the election's parent election... hmm... |
156 | * @return Election |
157 | */ |
158 | public function getElection() { |
159 | return $this; |
160 | } |
161 | |
162 | /** |
163 | * Get a list of child entity objects. See Entity. |
164 | * @return array |
165 | */ |
166 | public function getChildren() { |
167 | return $this->getQuestions(); |
168 | } |
169 | |
170 | /** |
171 | * Get the start date in MW internal form. |
172 | * @return string |
173 | */ |
174 | public function getStartDate() { |
175 | return $this->startDate; |
176 | } |
177 | |
178 | /** |
179 | * Get the end date in MW internal form. |
180 | * @return string |
181 | */ |
182 | public function getEndDate() { |
183 | return $this->endDate; |
184 | } |
185 | |
186 | /** |
187 | * Returns true if the election has started. |
188 | * @param string|bool $ts The reference timestamp, or false for now. |
189 | * @return bool |
190 | */ |
191 | public function isStarted( $ts = false ) { |
192 | if ( $ts === false ) { |
193 | $ts = wfTimestampNow(); |
194 | } |
195 | |
196 | return !$this->startDate || $ts >= $this->startDate; |
197 | } |
198 | |
199 | /** |
200 | * Returns true if the election has finished. |
201 | * @param string|bool $ts The reference timestamp, or false for now. |
202 | * @return bool |
203 | */ |
204 | public function isFinished( $ts = false ) { |
205 | if ( $ts === false ) { |
206 | $ts = wfTimestampNow(); |
207 | } |
208 | |
209 | return $this->endDate && $ts >= $this->endDate; |
210 | } |
211 | |
212 | /** |
213 | * Returns number of votes from an election. |
214 | * @return string |
215 | */ |
216 | public function getVotesCount() { |
217 | $dbr = $this->context->getDB( DB_REPLICA ); |
218 | |
219 | return $dbr->newSelectQueryBuilder() |
220 | ->select( 'COUNT(*)' ) |
221 | ->from( 'securepoll_votes' ) |
222 | ->where( [ |
223 | 'vote_election' => $this->getId(), |
224 | 'vote_current' => 1, |
225 | 'vote_struck' => 0, |
226 | ] ) |
227 | ->caller( __METHOD__ ) |
228 | ->fetchField(); |
229 | } |
230 | |
231 | /** |
232 | * Get the ballot object for this election. |
233 | * @return Ballot |
234 | */ |
235 | public function getBallot() { |
236 | if ( !$this->ballot ) { |
237 | $this->ballot = $this->context->newBallot( $this->ballotType, $this ); |
238 | } |
239 | |
240 | return $this->ballot; |
241 | } |
242 | |
243 | /** |
244 | * Determine whether a voter would be qualified to vote in this election, |
245 | * based on the given associative array of parameters. |
246 | * @param array $params Associative array |
247 | * @return Status |
248 | */ |
249 | public function getQualifiedStatus( $params ) { |
250 | $lang = RequestContext::getMain()->getLanguage(); |
251 | $props = $params['properties']; |
252 | $status = Status::newGood(); |
253 | |
254 | $lists = $props['lists'] ?? []; |
255 | $centralLists = $props['central-lists'] ?? []; |
256 | $includeList = $this->getProperty( 'include-list' ); |
257 | $excludeList = $this->getProperty( 'exclude-list' ); |
258 | |
259 | $includeUserGroups = explode( '|', $this->getProperty( 'allow-usergroups', '' ) ); |
260 | $inAllowedUserGroups = array_intersect( $includeUserGroups, $props['groups'] ); |
261 | |
262 | if ( $excludeList && in_array( $excludeList, $lists ) ) { |
263 | $status->fatal( 'securepoll-in-exclude-list' ); |
264 | } elseif ( ( $includeList && in_array( $includeList, $lists ) ) || |
265 | $inAllowedUserGroups ) { |
266 | // Good |
267 | } else { |
268 | // Edits |
269 | $minEdits = $this->getProperty( 'min-edits' ); |
270 | $edits = $props['edit-count'] ?? 0; |
271 | if ( $minEdits && $edits < $minEdits ) { |
272 | $status->fatal( |
273 | 'securepoll-too-few-edits', |
274 | $lang->formatNum( $minEdits ), |
275 | $lang->formatNum( $edits ) |
276 | ); |
277 | } |
278 | |
279 | // Registration date |
280 | $maxDate = $this->getProperty( 'max-registration' ); |
281 | $date = $props['registration'] ?? 0; |
282 | if ( $maxDate && $date > $maxDate ) { |
283 | $status->fatal( |
284 | 'securepoll-too-new', |
285 | $lang->date( $maxDate ), |
286 | $lang->date( $date ), |
287 | $lang->time( $maxDate ), |
288 | $lang->time( $date ) |
289 | ); |
290 | } |
291 | |
292 | // Blocked |
293 | $notAllowedSitewideBlocked = $this->getProperty( 'not-sitewide-blocked' ); |
294 | $notPartialBlocked = $this->getProperty( 'not-partial-blocked' ); |
295 | $isBlocked = !empty( $props['blocked'] ); |
296 | $isSitewideBlocked = $props['isSitewideBlocked'] ?? false; |
297 | if ( $notAllowedSitewideBlocked && $isBlocked && $isSitewideBlocked ) { |
298 | $status->fatal( 'securepoll-blocked' ); |
299 | } elseif ( $notPartialBlocked && $isBlocked && !$isSitewideBlocked ) { |
300 | $status->fatal( 'securepoll-blocked-partial' ); |
301 | } |
302 | |
303 | // Centrally blocked on more than X projects |
304 | $notCentrallyBlocked = $this->getProperty( 'not-centrally-blocked' ); |
305 | if ( $notPartialBlocked ) { |
306 | $centralBlockCount = $props['central-block-count'] ?? 0; |
307 | } else { |
308 | $centralBlockCount = $props['central-sitewide-block-count'] ?? 0; |
309 | } |
310 | $centralBlockThreshold = $this->getProperty( 'central-block-threshold', 1 ); |
311 | if ( $notCentrallyBlocked && $centralBlockCount >= $centralBlockThreshold ) { |
312 | $status->fatal( |
313 | 'securepoll-blocked-centrally', |
314 | $lang->formatNum( $centralBlockThreshold ) |
315 | ); |
316 | } |
317 | |
318 | // Bot |
319 | $notBot = $this->getProperty( 'not-bot' ); |
320 | $isBot = !empty( $props['bot'] ); |
321 | if ( $notBot && $isBot ) { |
322 | $status->fatal( 'securepoll-bot' ); |
323 | } |
324 | |
325 | // Groups |
326 | $needGroup = $this->getProperty( 'need-group' ); |
327 | $groups = $props['groups'] ?? []; |
328 | if ( $needGroup && !in_array( $needGroup, $groups ) ) { |
329 | $status->fatal( 'securepoll-not-in-group', $needGroup ); |
330 | } |
331 | |
332 | // Lists |
333 | $needList = $this->getProperty( 'need-list' ); |
334 | if ( $needList && !in_array( $needList, $lists ) ) { |
335 | $status->fatal( 'securepoll-not-in-list' ); |
336 | } |
337 | |
338 | $needCentralList = $this->getProperty( 'need-central-list' ); |
339 | if ( $needCentralList && !in_array( $needCentralList, $centralLists ) ) { |
340 | $status->fatal( 'securepoll-not-in-list' ); |
341 | } |
342 | } |
343 | |
344 | // Get custom error message and add it to the status's error messages |
345 | if ( !$status->isOK() ) { |
346 | $errorMsgText = $this->getMessage( 'unqualified-error' ); |
347 | if ( $errorMsgText !== '[unqualified-error]' && $errorMsgText !== '' ) { |
348 | // We create the message as a separate step so that possible wikitext in |
349 | // $errorMsgText gets parsed. |
350 | $errorMsg = wfMessage( 'securepoll-custom-unqualified', $errorMsgText ); |
351 | $status->error( $errorMsg ); |
352 | } |
353 | } |
354 | |
355 | return $status; |
356 | } |
357 | |
358 | /** |
359 | * Returns true if the user is an admin of the current election. |
360 | * @param Authority $authority |
361 | * @return bool |
362 | */ |
363 | public function isAdmin( Authority $authority ) { |
364 | $admins = array_map( 'trim', explode( '|', $this->getProperty( 'admins' ) ) ); |
365 | |
366 | return in_array( $authority->getUser()->getName(), $admins ) |
367 | && $authority->isAllowed( 'securepoll-edit-poll' ); |
368 | } |
369 | |
370 | /** |
371 | * Returns true if the voter has voted already. |
372 | * @param Voter $voter |
373 | * @return bool |
374 | */ |
375 | public function hasVoted( $voter ) { |
376 | $db = $this->context->getDB(); |
377 | $row = $db->newSelectQueryBuilder() |
378 | ->select( '1' ) |
379 | ->from( 'securepoll_votes' ) |
380 | ->where( [ |
381 | 'vote_election' => $this->getId(), |
382 | 'vote_voter' => $voter->getId(), |
383 | ] ) |
384 | ->caller( __METHOD__ ) |
385 | ->fetchRow(); |
386 | |
387 | return $row !== false; |
388 | } |
389 | |
390 | /** |
391 | * Returns true if the election allows voters to change their vote after it |
392 | * is initially cast. |
393 | * @return bool |
394 | */ |
395 | public function allowChange() { |
396 | return !$this->getProperty( 'disallow-change' ); |
397 | } |
398 | |
399 | /** |
400 | * Get the questions in this election |
401 | * @return Question[] |
402 | */ |
403 | public function getQuestions() { |
404 | if ( $this->questions === null ) { |
405 | $info = $this->context->getStore()->getQuestionInfo( $this->getId() ); |
406 | $this->questions = []; |
407 | foreach ( $info as $questionInfo ) { |
408 | $this->questions[] = $this->context->newQuestion( $questionInfo ); |
409 | } |
410 | } |
411 | |
412 | return $this->questions; |
413 | } |
414 | |
415 | /** |
416 | * Get the authorization object. |
417 | * @return Auth |
418 | */ |
419 | public function getAuth() { |
420 | if ( !$this->auth ) { |
421 | $this->auth = $this->context->newAuth( $this->authType ); |
422 | } |
423 | |
424 | return $this->auth; |
425 | } |
426 | |
427 | /** |
428 | * Get the primary language for this election. This language will be used as |
429 | * a default in the relevant places. |
430 | * @return string |
431 | */ |
432 | public function getLanguage() { |
433 | return $this->primaryLang; |
434 | } |
435 | |
436 | /** |
437 | * Get the cryptography module for this election, or false if none is |
438 | * defined. |
439 | * @return Crypt|false |
440 | * @throws InvalidDataException |
441 | */ |
442 | public function getCrypt() { |
443 | $type = $this->getProperty( 'encrypt-type', 'none' ); |
444 | try { |
445 | return $this->context->newCrypt( $type, $this ); |
446 | } catch ( InvalidArgumentException $e ) { |
447 | throw new InvalidDataException( 'Invalid encryption type' ); |
448 | } |
449 | } |
450 | |
451 | /** |
452 | * Get the tally type |
453 | * @return string |
454 | */ |
455 | public function getTallyType() { |
456 | return $this->tallyType; |
457 | } |
458 | |
459 | /** |
460 | * Call a callback function for each valid vote record, in random order. |
461 | * @param callable $callback |
462 | * @return Status |
463 | */ |
464 | public function dumpVotesToCallback( $callback ) { |
465 | $random = $this->context->getRandom(); |
466 | $status = $random->open(); |
467 | if ( !$status->isOK() ) { |
468 | return $status; |
469 | } |
470 | $db = $this->context->getDB(); |
471 | $res = $db->newSelectQueryBuilder() |
472 | ->select( '*' ) |
473 | ->from( 'securepoll_votes' ) |
474 | ->where( [ |
475 | 'vote_election' => $this->getId(), |
476 | 'vote_current' => 1, |
477 | 'vote_struck' => 0 |
478 | ] ) |
479 | ->caller( __METHOD__ ) |
480 | ->fetchResultSet(); |
481 | if ( $res->numRows() ) { |
482 | $order = $random->shuffle( range( 0, $res->numRows() - 1 ) ); |
483 | foreach ( $order as $i ) { |
484 | $res->seek( $i ); |
485 | $callback( $this, $res->fetchObject() ); |
486 | } |
487 | } |
488 | $random->close(); |
489 | |
490 | return Status::newGood(); |
491 | } |
492 | |
493 | /** |
494 | * Get an XML snippet describing the configuration of this object |
495 | * @param array $params |
496 | * @return string |
497 | */ |
498 | public function getConfXml( $params = [] ) { |
499 | $s = "<configuration>\n" . Xml::element( 'title', [], $this->title ) . "\n" . Xml::element( |
500 | 'ballot', |
501 | [], |
502 | $this->ballotType |
503 | ) . "\n" . Xml::element( 'tally', [], $this->tallyType ) . "\n" . Xml::element( |
504 | 'primaryLang', |
505 | [], |
506 | $this->primaryLang |
507 | ) . "\n" . Xml::element( |
508 | 'startDate', |
509 | [], |
510 | wfTimestamp( TS_ISO_8601, $this->startDate ) |
511 | ) . "\n" . Xml::element( |
512 | 'endDate', |
513 | [], |
514 | wfTimestamp( TS_ISO_8601, $this->endDate ) |
515 | ) . "\n" . $this->getConfXmlEntityStuff( $params ); |
516 | |
517 | // If we're making a jump dump, we need to add some extra properties, and |
518 | // override the auth type |
519 | if ( !empty( $params['jump'] ) ) { |
520 | $s .= Xml::element( 'auth', [], 'local' ) . "\n" . Xml::element( |
521 | 'property', |
522 | [ 'name' => 'jump-url' ], |
523 | $this->context->getSpecialTitle()->getCanonicalURL() |
524 | ) . "\n" . Xml::element( |
525 | 'property', |
526 | [ 'name' => 'jump-id' ], |
527 | (string)$this->getId() |
528 | ) . "\n"; |
529 | } else { |
530 | $s .= Xml::element( 'auth', [], $this->authType ) . "\n"; |
531 | } |
532 | |
533 | foreach ( $this->getQuestions() as $question ) { |
534 | $s .= $question->getConfXml( $params ); |
535 | } |
536 | $s .= "</configuration>\n"; |
537 | |
538 | return $s; |
539 | } |
540 | |
541 | /** |
542 | * Get property names which aren't included in an XML dump |
543 | * @param array $params |
544 | * @return array |
545 | */ |
546 | public function getPropertyDumpExclusion( $params = [] ) { |
547 | if ( empty( $params['private'] ) ) { |
548 | return [ |
549 | 'gpg-encrypt-key', |
550 | 'gpg-sign-key', |
551 | 'gpg-decrypt-key', |
552 | 'openssl-encrypt-key', |
553 | 'openssl-sign-key', |
554 | 'openssl-decrypt-key' |
555 | ]; |
556 | } else { |
557 | return []; |
558 | } |
559 | } |
560 | |
561 | /** |
562 | * Tally the valid votes for this election. |
563 | * Returns a Status object. On success, the value property will contain a |
564 | * ElectionTallier object. |
565 | * @return Status |
566 | */ |
567 | public function tally() { |
568 | $tallier = $this->context->newElectionTallier( $this ); |
569 | $status = $tallier->execute(); |
570 | if ( $status->isOK() ) { |
571 | return Status::newGood( $tallier ); |
572 | } else { |
573 | return $status; |
574 | } |
575 | } |
576 | |
577 | /** |
578 | * Get the stored tally results for this election. The caller can use the |
579 | * returned tallier to format the results in the desired way. |
580 | * |
581 | * @param IDatabase $dbr |
582 | * @return ElectionTallier|bool |
583 | */ |
584 | public function getTallyFromDb( $dbr ) { |
585 | $result = $dbr->newSelectQueryBuilder() |
586 | ->select( 'pr_value' ) |
587 | ->from( 'securepoll_properties' ) |
588 | ->where( [ |
589 | 'pr_entity' => $this->getId(), |
590 | 'pr_key' => 'tally-result', |
591 | ] ) |
592 | ->caller( __METHOD__ ) |
593 | ->fetchField(); |
594 | if ( !$result ) { |
595 | return false; |
596 | } |
597 | |
598 | $tallier = $this->context->newElectionTallier( $this ); |
599 | $tallier->loadJSONResult( json_decode( $result, true ) ); |
600 | return $tallier; |
601 | } |
602 | |
603 | /** |
604 | * Gets the time that the tally completed. If a tally has not been done yet |
605 | * then this function will return 'false'. |
606 | * |
607 | * @param IDatabase $dbr |
608 | * @return int|bool |
609 | */ |
610 | public function getTallyResultTimeFromDb( $dbr ) { |
611 | $time = $dbr->newSelectQueryBuilder() |
612 | ->select( 'pr_value' ) |
613 | ->from( 'securepoll_properties' ) |
614 | ->where( [ |
615 | 'pr_entity' => $this->getId(), |
616 | 'pr_key' => 'tally-result-time', |
617 | ] ) |
618 | ->caller( __METHOD__ ) |
619 | ->fetchField(); |
620 | if ( !$time ) { |
621 | return false; |
622 | } |
623 | |
624 | return (int)$time; |
625 | } |
626 | } |