Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Voter
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 17
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 newFromId
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 newFromRow
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 createVoter
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDomain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getElectionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLanguage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getProperty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isRemote
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 decodeProperties
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 encodeProperties
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doCookieCheck
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 addCookieDup
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\SecurePoll\User;
4
5use MediaWiki\Extension\SecurePoll\Context;
6use MediaWiki\WikiMap\WikiMap;
7use stdClass;
8
9/**
10 * Class representing a voter. A voter is associated with one election only. Voter
11 * properties include a snapshot of heuristic qualifications such as edit count.
12 */
13class Voter {
14    /** @var int|null */
15    public $id;
16    /** @var int|null */
17    public $electionId;
18    /** @var string|null */
19    public $name;
20    /** @var string|null */
21    public $domain;
22    /** @var string|null */
23    public $wiki;
24    /** @var string|null */
25    public $type;
26    /** @var string|null */
27    public $url;
28    /** @var array|null */
29    public $properties = [];
30    /** @var Context */
31    public $context;
32
33    /** @var string[] */
34    private static $paramNames = [
35        'id',
36        'electionId',
37        'name',
38        'domain',
39        'wiki',
40        'type',
41        'url',
42        'properties'
43    ];
44
45    /**
46     * Create a voter from the given associative array of parameters
47     * @param Context $context
48     * @param array $params
49     */
50    public function __construct( $context, $params ) {
51        $this->context = $context;
52        foreach ( self::$paramNames as $name ) {
53            if ( isset( $params[$name] ) ) {
54                $this->$name = $params[$name];
55            }
56        }
57    }
58
59    /**
60     * Create a voter object from the database
61     * @param Context $context
62     * @param int $id
63     * @param int $index DB_PRIMARY or DB_REPLICA
64     * @return Voter|bool false if the ID is not valid
65     */
66    public static function newFromId( $context, $id, $index = DB_PRIMARY ) {
67        $db = $context->getDB( $index );
68        $row = $db->newSelectQueryBuilder()
69            ->select( '*' )
70            ->from( 'securepoll_voters' )
71            ->where( [ 'voter_id' => $id ] )
72            ->caller( __METHOD__ )
73            ->fetchRow();
74        if ( !$row ) {
75            return false;
76        }
77
78        return self::newFromRow( $context, $row );
79    }
80
81    /**
82     * Create a voter from a DB result row
83     * @param Context $context
84     * @param stdClass $row
85     * @return self
86     */
87    public static function newFromRow( $context, $row ) {
88        return new self(
89            $context, [
90                'id' => $row->voter_id,
91                'electionId' => $row->voter_election,
92                'name' => $row->voter_name,
93                'domain' => $row->voter_domain,
94                'type' => $row->voter_type,
95                'url' => $row->voter_url,
96                'properties' => self::decodeProperties( $row->voter_properties )
97            ]
98        );
99    }
100
101    /**
102     * Create a voter with the given parameters. Assumes the voter does not exist,
103     * and inserts it into the database.
104     *
105     * The row needs to be locked before this function is called, to avoid
106     * duplicate key errors.
107     * @param Context $context
108     * @param array $params
109     * @return self
110     */
111    public static function createVoter( $context, $params ) {
112        $db = $context->getDB();
113        $row = [
114            'voter_election' => $params['electionId'],
115            'voter_name' => $params['name'],
116            'voter_type' => $params['type'],
117            'voter_domain' => $params['domain'],
118            'voter_url' => $params['url'],
119            'voter_properties' => self::encodeProperties( $params['properties'] )
120        ];
121        $db->newInsertQueryBuilder()
122            ->insertInto( 'securepoll_voters' )
123            ->row( $row )
124            ->caller( __METHOD__ )
125            ->execute();
126        $params['id'] = $db->insertId();
127
128        return new self( $context, $params );
129    }
130
131    /**
132     * Get the voter ID
133     * @return int
134     */
135    public function getId() {
136        return $this->id;
137    }
138
139    /**
140     * Get the voter name. This is a short, ambiguous name appropriate for
141     * display.
142     * @return string
143     */
144    public function getName() {
145        return $this->name;
146    }
147
148    /**
149     * Get the authorization type.
150     * @return string
151     */
152    public function getType() {
153        return $this->type;
154    }
155
156    /**
157     * Get the voter domain. The name and domain, taken together, should usually be
158     * unique, although this is not strictly necessary.
159     * @return string
160     */
161    public function getDomain() {
162        return $this->domain;
163    }
164
165    /**
166     * Get a URL uniquely identifying the underlying user.
167     * @return string
168     */
169    public function getUrl() {
170        return $this->url;
171    }
172
173    /**
174     * Get the associated election ID
175     * @return int
176     */
177    public function getElectionId() {
178        return $this->electionId;
179    }
180
181    /**
182     * Get the voter's preferred language
183     * @return mixed
184     */
185    public function getLanguage() {
186        return $this->getProperty( 'language', 'en' );
187    }
188
189    /**
190     * Get a property from the property blob
191     * @param string $name
192     * @param string|false $default
193     * @return mixed
194     */
195    public function getProperty( $name, $default = false ) {
196        return $this->properties[$name] ?? $default;
197    }
198
199    /**
200     * Returns true if the voter is a guest user.
201     * @return bool
202     */
203    public function isRemote() {
204        return $this->type !== 'local';
205    }
206
207    /**
208     * Decode the properties blob to produce an associative array.
209     * @param string $blob
210     * @return array
211     */
212    public static function decodeProperties( $blob ) {
213        if ( strval( $blob ) == '' ) {
214            return [];
215        } else {
216            return unserialize( $blob );
217        }
218    }
219
220    /**
221     * Encode an associative array of properties to a blob suitable for storing
222     * in the database.
223     * @param array $props
224     * @return string
225     */
226    public static function encodeProperties( $props ) {
227        return serialize( $props );
228    }
229
230    public function doCookieCheck() {
231        $cookieName = WikiMap::getCurrentWikiId() . '_securepoll_check';
232        if ( isset( $_COOKIE[$cookieName] ) ) {
233            $otherVoterId = intval( $_COOKIE[$cookieName] );
234            if ( $otherVoterId != $this->getId() ) {
235                $otherVoter = self::newFromId( $this->context, $otherVoterId );
236                if ( $otherVoter && $otherVoter->getElectionId() == $this->getElectionId() ) {
237                    $this->addCookieDup( $otherVoterId );
238                }
239            }
240        } else {
241            setcookie( $cookieName, (string)$this->getId(), time() + 86400 * 30 );
242        }
243    }
244
245    /**
246     * Flag a duplicate voter
247     * @param int $voterId
248     */
249    public function addCookieDup( $voterId ) {
250        $dbw = $this->context->getDB();
251        # Insert the log record
252        $dbw->newInsertQueryBuilder()
253            ->insertInto( 'securepoll_cookie_match' )
254            ->row( [
255                'cm_election' => $this->getElectionId(),
256                'cm_voter_1' => $this->getId(),
257                'cm_voter_2' => $voterId,
258                'cm_timestamp' => wfTimestampNow()
259            ] )
260            ->caller( __METHOD__ )
261            ->execute();
262
263        # Update the denormalized fields
264        $dbw->newUpdateQueryBuilder()
265            ->update( 'securepoll_votes' )
266            ->set( [ 'vote_cookie_dup' => 1 ] )
267            ->where( [
268                'vote_election' => $this->getElectionId(),
269                'vote_voter' => [
270                    $this->getId(),
271                    $voterId
272                ]
273            ] )
274            ->caller( __METHOD__ )
275            ->execute();
276    }
277
278}