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