Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
Context
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 30
2550
0.00% covered (danger)
0.00%
0 / 1
 newFromXmlFile
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getParserOptions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getStore
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getSpecialTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setStore
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getEntityType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getElection
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getElectionByTitle
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 newElectionFromRow
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newVoterFromRow
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createVoter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVoter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRandom
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setLanguages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMessages
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getMessage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newElection
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newQuestion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newOption
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newCrypt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newTallier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newBallot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBallotTypesForTally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBallotTypesForVote
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 newAuth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newVoter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newElectionTallier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 varDump
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 getResourceUrl
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\SecurePoll;
4
5use MediaWiki\Context\RequestContext;
6use MediaWiki\Extension\SecurePoll\Ballots\Ballot;
7use MediaWiki\Extension\SecurePoll\Crypt\Crypt;
8use MediaWiki\Extension\SecurePoll\Crypt\Random;
9use MediaWiki\Extension\SecurePoll\Entities\Election;
10use MediaWiki\Extension\SecurePoll\Entities\Option;
11use MediaWiki\Extension\SecurePoll\Entities\Question;
12use MediaWiki\Extension\SecurePoll\Store\DBStore;
13use MediaWiki\Extension\SecurePoll\Store\Store;
14use MediaWiki\Extension\SecurePoll\Store\XMLStore;
15use MediaWiki\Extension\SecurePoll\Talliers\ElectionTallier;
16use MediaWiki\Extension\SecurePoll\Talliers\Tallier;
17use MediaWiki\Extension\SecurePoll\User\Auth;
18use MediaWiki\Extension\SecurePoll\User\LocalAuth;
19use MediaWiki\Extension\SecurePoll\User\RemoteMWAuth;
20use MediaWiki\Extension\SecurePoll\User\Voter;
21use MediaWiki\MainConfigNames;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Parser\ParserOptions;
24use MediaWiki\SpecialPage\SpecialPage;
25use MediaWiki\Title\Title;
26use stdClass;
27use Wikimedia\Rdbms\IDatabase;
28
29/**
30 * This object contains caches and various items of processing context for
31 * SecurePoll. It manages instances of long-lived objects such as the
32 * Store subclass.
33 *
34 * Long-lived data should be stored here, rather than in global variables or
35 * static member variables.
36 *
37 * A context object is passed to almost all SecurePoll constructors. This class
38 * provides factory functions for these objects.
39 *
40 * For debugging purposes, a var_dump() workalike which omits context objects
41 * is available as $context->varDump().
42 */
43class Context {
44    /** @var string[] Language fallback sequence */
45    public $languages = [ 'en' ];
46
47    /**
48     * Message text cache
49     * @var string[][][]
50     */
51    public $messageCache = [];
52
53    /** @var array election cache */
54    public $electionCache = [];
55
56    /**
57     * @var array Which messages are loaded. 2-d array: language and entity ID, value arbitrary.
58     */
59    public $messagesLoaded = [];
60
61    /** @var ParserOptions|null ParserOptions instance used for message parsing */
62    public $parserOptions;
63
64    /**
65     * Key value store of data needed for a decryption method. Data is added and
66     * used by a decryption object and persists across different decryption
67     * object instances.
68     *
69     * @var array
70     */
71    public $decryptData = [];
72
73    /** @var Store|null The store object */
74    public $store;
75
76    /** @var Random|null The Random instance */
77    public $random;
78
79    /** @var string[] */
80    private $ballotTypesForVote;
81
82    /**
83     * Create a new Context with an XML file as the storage backend.
84     * Returns false if there was a problem with the file, like a parse error.
85     * @param string $fileName
86     * @return bool|self
87     */
88    public static function newFromXmlFile( $fileName ) {
89        $context = new self;
90        $store = new XMLStore( $fileName );
91        $context->setStore( $store );
92        $success = $store->readFile();
93        if ( $success ) {
94            return $context;
95        } else {
96            return false;
97        }
98    }
99
100    /**
101     * Get the ParserOptions instance
102     * @return ParserOptions
103     */
104    public function getParserOptions() {
105        if ( !$this->parserOptions ) {
106            $this->parserOptions = ParserOptions::newFromUser(
107                RequestContext::getMain()->getUser()
108            );
109        }
110
111        return $this->parserOptions;
112    }
113
114    /**
115     * Get the Store instance
116     * @return Store
117     */
118    public function getStore() {
119        if ( $this->store === null ) {
120            $this->store = new DBStore(
121                MediaWikiServices::getInstance()->getDBLoadBalancer(),
122                false
123            );
124        }
125
126        return $this->store;
127    }
128
129    /**
130     * Get a Title object for Special:SecurePoll
131     * @param string|false $subpage
132     * @return Title
133     */
134    public function getSpecialTitle( $subpage = false ) {
135        return SpecialPage::getTitleFor( 'SecurePoll', $subpage );
136    }
137
138    /**
139     * Set the store object. Overrides any previous store class.
140     * @param Store $store
141     */
142    public function setStore( $store ) {
143        $this->messageCache = $this->messagesLoaded = [];
144        $this->store = $store;
145    }
146
147    /**
148     * Get the type of a particular entity
149     * @param int $id
150     * @return string|false
151     */
152    public function getEntityType( $id ) {
153        return $this->getStore()->getEntityType( $id );
154    }
155
156    /**
157     * Get an election object from the store, with a given entity ID. Returns
158     * false if it does not exist.
159     * @param int $id
160     * @return Election|bool
161     */
162    public function getElection( $id ) {
163        if ( !isset( $this->electionCache[$id] ) ) {
164            $info = $this->getStore()->getElectionInfo( [ $id ] );
165            if ( $info ) {
166                $this->electionCache[$id] = $this->newElection( reset( $info ) );
167            } else {
168                $this->electionCache[$id] = false;
169            }
170        }
171
172        return $this->electionCache[$id];
173    }
174
175    /**
176     * Get an election object from the store, with a given name. Returns false
177     * if there is no such election.
178     * @param string $name
179     * @return Election|false
180     */
181    public function getElectionByTitle( $name ) {
182        $info = $this->getStore()->getElectionInfoByTitle( [ $name ] );
183        if ( $info ) {
184            return $this->newElection( reset( $info ) );
185        } else {
186            return false;
187        }
188    }
189
190    /**
191     * Get an election object from a securepoll_elections DB row. This will fail
192     * if the current store class does not support database operations.
193     * @param stdClass $row
194     * @return Election
195     */
196    public function newElectionFromRow( $row ) {
197        $info = $this->getStore()->decodeElectionRow( $row );
198
199        return $this->newElection( $info );
200    }
201
202    /**
203     * Get a voter object from a securepoll_voters row
204     * @param stdClass $row
205     * @return Voter
206     */
207    public function newVoterFromRow( $row ) {
208        return Voter::newFromRow( $this, $row );
209    }
210
211    /**
212     * Create a voter with the given parameters. Assumes the voter does not exist,
213     * and inserts it into the database.
214     *
215     * The row needs to be locked before this function is called, to avoid
216     * duplicate key errors.
217     * @param array $params
218     * @return Voter
219     */
220    public function createVoter( $params ) {
221        return Voter::createVoter( $this, $params );
222    }
223
224    /**
225     * Create a voter object from the database
226     * @param int $id
227     * @param int $index DB_PRIMARY or DB_REPLICA
228     * @return Voter|false false if the ID is not valid
229     */
230    public function getVoter( $id, $index = DB_PRIMARY ) {
231        return Voter::newFromId( $this, $id, $index );
232    }
233
234    /**
235     * Get a Random instance. This provides cryptographic random
236     * number generation.
237     * @return Random
238     */
239    public function getRandom() {
240        if ( !$this->random ) {
241            $this->random = new Random;
242        }
243
244        return $this->random;
245    }
246
247    /**
248     * Set the global language fallback sequence.
249     *
250     * @param array $languages A list of language codes. When a message is
251     *     requested, the first code in the array will be tried first, followed
252     *     by the subsequent codes.
253     */
254    public function setLanguages( $languages ) {
255        $this->languages = $languages;
256    }
257
258    /**
259     * Get some messages from the backend store or the cache.
260     * This is an internal interface for Entity, generally you
261     * should use Entity::getMessage() instead.
262     *
263     * @param string $lang Language code
264     * @param array $ids Entity IDs
265     * @return string[][]
266     */
267    public function getMessages( $lang, $ids ) {
268        if ( isset( $this->messagesLoaded[$lang] ) ) {
269            $cacheRow = $this->messagesLoaded[$lang];
270            $uncachedIds = array_flip( $ids );
271            foreach ( $uncachedIds as $id => $unused ) {
272                if ( isset( $cacheRow[$id] ) ) {
273                    unset( $uncachedIds[$id] );
274                }
275            }
276            if ( count( $uncachedIds ) ) {
277                $messages = $this->getStore()->getMessages( $lang, array_keys( $uncachedIds ) );
278                $this->messageCache[$lang] += $messages;
279                $this->messagesLoaded[$lang] += $uncachedIds;
280            }
281
282            return array_intersect_key( $this->messageCache[$lang], array_flip( $ids ) );
283        } else {
284            $this->messagesLoaded[$lang] = $ids;
285            $this->messageCache[$lang] = $this->getStore()->getMessages( $lang, $ids );
286
287            return $this->messageCache[$lang];
288        }
289    }
290
291    /**
292     * Get a particular message.
293     * This is an internal interface for Entity, generally you
294     * should use Entity::getMessage() instead.
295     *
296     * @param string $lang Language code
297     * @param string|int $id Entity ID
298     * @param string $key Message key
299     * @return string|false
300     */
301    public function getMessage( $lang, $id, $key ) {
302        if ( !isset( $this->messagesLoaded[$lang][$id] ) ) {
303            $this->getMessages( $lang, [ $id ] );
304        }
305
306        return $this->messageCache[$lang][$id][$key] ?? false;
307    }
308
309    /**
310     * Get a database object, or throw an exception if the current store object
311     * does not support database operations.
312     * @param int $index DB_PRIMARY or DB_REPLICA
313     * @return IDatabase
314     */
315    public function getDB( $index = DB_PRIMARY ) {
316        return $this->getStore()->getDB( $index );
317    }
318
319    /**
320     * @param array $info
321     * @return Election
322     */
323    public function newElection( $info ) {
324        return new Election( $this, $info );
325    }
326
327    /**
328     * @param array $info
329     * @return Question
330     */
331    public function newQuestion( $info ) {
332        return new Question( $this, $info );
333    }
334
335    /**
336     * @param array $info
337     * @return Option
338     */
339    public function newOption( $info ) {
340        return new Option( $this, $info );
341    }
342
343    /**
344     * @param string $type
345     * @param Election $election
346     * @return Crypt|false False when encryption type is set to "none"
347     */
348    public function newCrypt( $type, $election ) {
349        return Crypt::factory( $this, $type, $election );
350    }
351
352    /**
353     * @param string $type
354     * @param ElectionTallier $electionTallier
355     * @param Question $question
356     * @return Tallier
357     */
358    public function newTallier( $type, $electionTallier, $question ) {
359        return Tallier::factory( $this, $type, $electionTallier, $question );
360    }
361
362    /**
363     * @param string $type
364     * @param Election $election
365     * @return Ballot
366     */
367    public function newBallot( $type, $election ) {
368        return Ballot::factory( $this, $type, $election );
369    }
370
371    /**
372     * Get a map of ballot type names to classes. Include all the ballot
373     * types that may be found in the securepoll_votes table.
374     *
375     * @return array<string,class-string<Ballot>>
376     */
377    public function getBallotTypesForTally() {
378        return Ballot::BALLOT_TYPES;
379    }
380
381    /**
382     * Get a map of ballot type names to classes, for all ballot types which
383     * are valid for new votes and election creation.
384     *
385     * @return array<string,class-string<Ballot>>
386     */
387    public function getBallotTypesForVote() {
388        if ( $this->ballotTypesForVote === null ) {
389            $types = $this->getBallotTypesForTally();
390
391            // Archived (T300087)
392            unset( $types['radio-range-comment' ] );
393
394            $this->ballotTypesForVote = $types;
395        }
396        return $this->ballotTypesForVote;
397    }
398
399    /**
400     * @param string $type
401     * @return LocalAuth|RemoteMWAuth
402     */
403    public function newAuth( $type ) {
404        return Auth::factory( $this, $type );
405    }
406
407    /**
408     * @param array $params
409     * @return Voter
410     */
411    public function newVoter( $params ) {
412        return new Voter( $this, $params );
413    }
414
415    /**
416     * @param Election $election
417     * @return ElectionTallier
418     */
419    public function newElectionTallier( $election ) {
420        return new ElectionTallier( $this, $election );
421    }
422
423    /**
424     * Debugging function to output a representation of a mixed-type variable,
425     * but omitting the $obj->context member variables for brevity.
426     *
427     * @param mixed $var
428     * @param bool $return True to return the text instead of echoing
429     * @param int $level Recursion level, leave this as zero when calling.
430     * @return string|void
431     */
432    public function varDump( $var, $return = false, $level = 0 ) {
433        $tab = '    ';
434        $indent = str_repeat( $tab, $level );
435        if ( is_array( $var ) ) {
436            $s = "array(\n";
437            foreach ( $var as $key => $value ) {
438                $s .= "$indent$tab" . $this->varDump(
439                        $key,
440                        true,
441                        $level + 1
442                    ) . " => " . $this->varDump( $value, true, $level + 1 ) . ",\n";
443            }
444            $s .= "{$indent})";
445        } elseif ( is_object( $var ) ) {
446            $props = (array)$var;
447            $s = get_class( $var ) . " {\n";
448            foreach ( $props as $key => $value ) {
449                $s .= "$indent$tab" . $this->varDump( $key, true, $level + 1 ) . " => ";
450                if ( $key === 'context' ) {
451                    $s .= "[CONTEXT],\n";
452                } else {
453                    $s .= $this->varDump( $value, true, $level + 1 ) . ",\n";
454                }
455            }
456            $s .= "{$indent}}";
457        } else {
458            $s = var_export( $var, true );
459        }
460        if ( $level == 0 ) {
461            $s .= "\n";
462        }
463        if ( $return ) {
464            return $s;
465        } else {
466            echo $s;
467        }
468    }
469
470    /**
471     * @param string $resource
472     * @return string
473     */
474    public function getResourceUrl( $resource ) {
475        $config = MediaWikiServices::getInstance()->getMainConfig();
476        $extensionAssetsPath = $config->get( MainConfigNames::ExtensionAssetsPath );
477
478        return "$extensionAssetsPath/SecurePoll/resources/$resource";
479    }
480}