Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
TallyElectionJob
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 7
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 allowRetries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 doRun
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 preRun
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 postRun
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 markAsFailed
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\Jobs;
4
5use Job;
6use MediaWiki\Extension\SecurePoll\Context;
7use MediaWiki\Extension\SecurePoll\Entities\Election;
8use MediaWiki\Extension\SecurePoll\Exceptions\InvalidDataException;
9use MediaWiki\Extension\SecurePoll\Talliers\ElectionTallier;
10use Throwable;
11use Wikimedia\Rdbms\IDatabase;
12
13/**
14 * Job for tallying an encrypted election.
15 */
16class TallyElectionJob extends Job {
17
18    /** @var int */
19    private $electionId;
20
21    /** @var Election|bool */
22    private $election;
23
24    /** @var IDatabase|null */
25    private $dbw;
26
27    /**
28     * @inheritDoc
29     */
30    public function __construct( $title, $params ) {
31        parent::__construct( 'securePollTallyElection', $title, $params );
32    }
33
34    /**
35     * @inheritDoc
36     */
37    public function allowRetries() {
38        return false;
39    }
40
41    /**
42     * @inheritDoc
43     */
44    public function run(): bool {
45        $context = new Context();
46
47        $this->electionId = (int)$this->params['electionId'];
48        $this->election = $context->getElection( $this->electionId );
49
50        if ( !$this->election ) {
51            $this->setLastError( "Could not get election '$this->electionId'" );
52
53            return false;
54        }
55
56        $this->dbw = $context->getDB( DB_PRIMARY );
57
58        try {
59            $this->preRun();
60
61            return $this->doRun();
62        } catch ( Throwable $e ) {
63            $this->markAsFailed( get_class( $e ) . ': ' . $e->getMessage(), __METHOD__ );
64
65            // Return here rather than re-throw the exception so that the explicit transaction
66            // round created for this job is not moved into the error state before running
67            // TallyElectionJob::postRun().
68            return false;
69        } finally {
70            $this->postRun();
71        }
72    }
73
74    /**
75     * @return bool
76     */
77    private function doRun(): bool {
78        $status = $this->election->tally();
79
80        if ( !$status->isOK() ) {
81            $this->markAsFailed( $status->getMessage(), __METHOD__ );
82
83            return false;
84        }
85
86        $tallier = $status->value;
87        '@phan-var ElectionTallier $tallier'; /** @var ElectionTallier $tallier */
88        $result = json_encode( $tallier->getJSONResult() );
89        $time = time();
90
91        $this->dbw->newReplaceQueryBuilder()
92            ->replaceInto( 'securepoll_properties' )
93            ->uniqueIndexFields( [ 'pr_entity', 'pr_key' ] )
94            ->row( [
95                'pr_entity' => $this->electionId,
96                'pr_key' => 'tally-result',
97                'pr_value' => $result,
98            ] )
99            ->row( [
100                'pr_entity' => $this->electionId,
101                'pr_key' => 'tally-result-time',
102                'pr_value' => $time,
103            ] )
104            ->caller( __METHOD__ )
105            ->execute();
106
107        return true;
108    }
109
110    /**
111     * Initializes the database for tallying the election by removing any previously recorded
112     * tallying errors and/or results.
113     */
114    private function preRun() {
115        $this->dbw->newDeleteQueryBuilder()
116            ->deleteFrom( 'securepoll_properties' )
117            ->where( [
118                'pr_entity' => $this->electionId,
119                'pr_key' => [
120                    'tally-error',
121                ],
122            ] )
123            ->caller( __METHOD__ )
124            ->execute();
125    }
126
127    private function postRun() {
128        $this->dbw->newDeleteQueryBuilder()
129            ->deleteFrom( 'securepoll_properties' )
130            ->where( [
131                'pr_entity' => $this->electionId,
132                'pr_key' => 'tally-job-enqueued',
133            ] )
134            ->caller( __METHOD__ )
135            ->execute();
136
137        try {
138            $crypt = $this->election->getCrypt();
139            if ( $crypt ) {
140                $crypt->cleanupDbForTallyJob( $this->electionId, $this->dbw );
141            }
142        } catch ( InvalidDataException $e ) {
143            // Election::getCrypt() throws InvalidDataException if an election has the "encrypt-type"
144            // property set but the corresponding class cannot be instantiated.
145            //
146            // Swallow this exception for the following reasons:
147            //
148            // * This job can only be enqueued when the user clicks the "Create tally" button on
149            //   the tally page. That page does not work in these circumstances.
150            //
151            // * At this point, the election has been tallied and the result can be displayed to
152            //   the user. If this exception is caught by the job runner then the explicit
153            //   transaction round created for this job will not be committed.
154        }
155    }
156
157    /**
158     * @param string $message
159     * @param string $fname
160     */
161    private function markAsFailed( string $message, string $fname ) {
162        $this->setLastError( $message );
163
164        $this->dbw->newInsertQueryBuilder()
165            ->insertInto( 'securepoll_properties' )
166            ->row( [
167                'pr_entity' => $this->electionId,
168                'pr_key' => 'tally-error',
169                'pr_value' => $message
170            ] )
171            ->caller( $fname )
172            ->execute();
173    }
174}