Index: trunk/extensions/SecurePoll/auth-api.php |
— | — | @@ -37,6 +37,8 @@ |
38 | 38 | echo serialize( Status::newFatal( 'securepoll-api-token-mismatch' ) ); |
39 | 39 | exit; |
40 | 40 | } |
41 | | -$status = Status::newGood( SecurePoll_LocalAuth::getUserParams( $user ) ); |
| 41 | +$context = new SecurePoll_Context; |
| 42 | +$auth = $context->newAuth( 'local' ); |
| 43 | +$status = Status::newGood( $auth->getUserParams( $user ) ); |
42 | 44 | echo serialize( $status ); |
43 | 45 | |
Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php |
— | — | @@ -151,6 +151,8 @@ |
152 | 152 | 'securepoll-tally-upload-submit' => 'Create tally', |
153 | 153 | 'securepoll-tally-error' => 'Error interpreting vote record, cannot produce a tally.', |
154 | 154 | 'securepoll-no-upload' => 'No file was uploaded, cannot tally results.', |
| 155 | + 'securepoll-dump-corrupt' => 'The dump file is corrupt and cannot be processed.', |
| 156 | + 'securepoll-tally-upload-error' => 'Error tallying dump file: $1', |
155 | 157 | ); |
156 | 158 | |
157 | 159 | /** Message documentation (Message documentation) |
Index: trunk/extensions/SecurePoll/SecurePoll.php |
— | — | @@ -50,7 +50,6 @@ |
51 | 51 | $wgSpecialPages['SecurePoll'] = 'SecurePoll_BasePage'; |
52 | 52 | |
53 | 53 | $wgAutoloadClasses = $wgAutoloadClasses + array( |
54 | | - 'SecurePoll' => "$dir/includes/Base.php", |
55 | 54 | 'SecurePoll_Auth' => "$dir/includes/Auth.php", |
56 | 55 | 'SecurePoll_LocalAuth' => "$dir/includes/Auth.php", |
57 | 56 | 'SecurePoll_RemoteMWAuth' => "$dir/includes/Auth.php", |
— | — | @@ -58,11 +57,13 @@ |
59 | 58 | 'SecurePoll_BasePage' => "$dir/includes/Base.php", |
60 | 59 | 'SecurePoll_ChooseBallot' => "$dir/includes/Ballot.php", |
61 | 60 | 'SecurePoll_PreferentialBallot' => "$dir/includes/Ballot.php", |
| 61 | + 'SecurePoll_Context' => "$dir/includes/Context.php", |
62 | 62 | 'SecurePoll_Crypt' => "$dir/includes/Crypt.php", |
63 | 63 | 'SecurePoll_GpgCrypt' => "$dir/includes/Crypt.php", |
64 | 64 | 'SecurePoll_DetailsPage' => "$dir/includes/DetailsPage.php", |
65 | 65 | 'SecurePoll_DumpPage' => "$dir/includes/DumpPage.php", |
66 | 66 | 'SecurePoll_Election' => "$dir/includes/Election.php", |
| 67 | + 'SecurePoll_ElectionTallier' => "$dir/includes/ElectionTallier.php", |
67 | 68 | 'SecurePoll_Entity' => "$dir/includes/Entity.php", |
68 | 69 | 'SecurePoll_EntryPage' => "$dir/includes/EntryPage.php", |
69 | 70 | 'SecurePoll_ListPage' => "$dir/includes/ListPage.php", |
— | — | @@ -72,6 +73,10 @@ |
73 | 74 | 'SecurePoll_Page' => "$dir/includes/Page.php", |
74 | 75 | 'SecurePoll_Question' => "$dir/includes/Question.php", |
75 | 76 | 'SecurePoll_Random' => "$dir/includes/Random.php", |
| 77 | + 'SecurePoll_Store' => "$dir/includes/Store.php", |
| 78 | + 'SecurePoll_DBStore' => "$dir/includes/Store.php", |
| 79 | + 'SecurePoll_MemoryStore' => "$dir/includes/Store.php", |
| 80 | + 'SecurePoll_XMLStore' => "$dir/includes/Store.php", |
76 | 81 | 'SecurePoll_Tallier' => "$dir/includes/Tallier.php", |
77 | 82 | 'SecurePoll_PluralityTallier' => "$dir/includes/Tallier.php", |
78 | 83 | 'SecurePoll_TallyPage' => "$dir/includes/TallyPage.php", |
Index: trunk/extensions/SecurePoll/includes/Voter.php |
— | — | @@ -13,7 +13,8 @@ |
14 | 14 | /** |
15 | 15 | * Create a voter from the given associative array of parameters |
16 | 16 | */ |
17 | | - function __construct( $params ) { |
| 17 | + function __construct( $context, $params ) { |
| 18 | + $this->context = $context; |
18 | 19 | foreach ( self::$paramNames as $name ) { |
19 | 20 | if ( isset( $params[$name] ) ) { |
20 | 21 | $this->$name = $params[$name]; |
— | — | @@ -25,20 +26,20 @@ |
26 | 27 | * Create a voter object from the database |
27 | 28 | * @return SecurePoll_Voter or false if the ID is not valid |
28 | 29 | */ |
29 | | - static function newFromId( $id ) { |
30 | | - $db = wfGetDB( DB_MASTER ); |
| 30 | + static function newFromId( $context, $id ) { |
| 31 | + $db = $context->getDB(); |
31 | 32 | $row = $db->selectRow( 'securepoll_voters', '*', array( 'voter_id' => $id ), __METHOD__ ); |
32 | 33 | if ( !$row ) { |
33 | 34 | return false; |
34 | 35 | } |
35 | | - return self::newFromRow( $row ); |
| 36 | + return self::newFromRow( $context, $row ); |
36 | 37 | } |
37 | 38 | |
38 | 39 | /** |
39 | 40 | * Create a voter from a DB result row |
40 | 41 | */ |
41 | | - static function newFromRow( $row ) { |
42 | | - return new self( array( |
| 42 | + static function newFromRow( $context, $row ) { |
| 43 | + return new self( $context, array( |
43 | 44 | 'id' => $row->voter_id, |
44 | 45 | 'electionId' => $row->voter_election, |
45 | 46 | 'name' => $row->voter_name, |
— | — | @@ -56,8 +57,8 @@ |
57 | 58 | * The row needs to be locked before this function is called, to avoid |
58 | 59 | * duplicate key errors. |
59 | 60 | */ |
60 | | - static function createVoter( $params ) { |
61 | | - $db = wfGetDB( DB_MASTER ); |
| 61 | + static function createVoter( $context, $params ) { |
| 62 | + $db = $context->getDB(); |
62 | 63 | $id = $db->nextSequenceValue( 'voters_voter_id' ); |
63 | 64 | $row = array( |
64 | 65 | 'voter_id' => $id, |
— | — | @@ -70,7 +71,7 @@ |
71 | 72 | ); |
72 | 73 | $db->insert( 'securepoll_voters', $row, __METHOD__ ); |
73 | 74 | $params['id'] = $db->insertId(); |
74 | | - return new self( $params ); |
| 75 | + return new self( $context, $params ); |
75 | 76 | } |
76 | 77 | |
77 | 78 | /** Get the voter ID */ |
— | — | @@ -152,7 +153,7 @@ |
153 | 154 | if ( isset( $_COOKIE[$cookieName] ) ) { |
154 | 155 | $otherVoterId = intval( $_COOKIE[$cookieName] ); |
155 | 156 | if ( $otherVoterId != $this->getId() ) { |
156 | | - $otherVoter = self::newFromId( $otherVoterId ); |
| 157 | + $otherVoter = self::newFromId( $this->context, $otherVoterId ); |
157 | 158 | if ( $otherVoter->getElectionId() == $this->getElectionId() ) { |
158 | 159 | $this->addCookieDup( $otherVoterId ); |
159 | 160 | } |
— | — | @@ -166,7 +167,7 @@ |
167 | 168 | * Flag a duplicate voter |
168 | 169 | */ |
169 | 170 | function addCookieDup( $voterId ) { |
170 | | - $dbw = wfGetDB( DB_MASTER ); |
| 171 | + $dbw = $this->context->getDB(); |
171 | 172 | # Insert the log record |
172 | 173 | $dbw->insert( 'securepoll_cookie_match', |
173 | 174 | array( |
Index: trunk/extensions/SecurePoll/includes/Auth.php |
— | — | @@ -4,6 +4,8 @@ |
5 | 5 | * Class for handling guest logins and sessions. Creates SecurePoll_Voter objects. |
6 | 6 | */ |
7 | 7 | class SecurePoll_Auth { |
| 8 | + var $context; |
| 9 | + |
8 | 10 | /** |
9 | 11 | * List of available authorisation modules (subclasses) |
10 | 12 | */ |
— | — | @@ -16,14 +18,18 @@ |
17 | 19 | * Create an auth object of the given type |
18 | 20 | * @param $type string |
19 | 21 | */ |
20 | | - static function factory( $type ) { |
| 22 | + static function factory( $context, $type ) { |
21 | 23 | if ( !isset( self::$authTypes[$type] ) ) { |
22 | 24 | throw new MWException( "Invalid authentication type: $type" ); |
23 | 25 | } |
24 | 26 | $class = self::$authTypes[$type]; |
25 | | - return new $class; |
| 27 | + return new $class( $context ); |
26 | 28 | } |
27 | 29 | |
| 30 | + function __construct( $context ) { |
| 31 | + $this->context = $context; |
| 32 | + } |
| 33 | + |
28 | 34 | /** |
29 | 35 | * Create a voter transparently, without user interaction. |
30 | 36 | * Sessions authorised against local accounts are created this way. |
— | — | @@ -68,7 +74,7 @@ |
69 | 75 | } |
70 | 76 | |
71 | 77 | # Sanity check election ID |
72 | | - $voter = SecurePoll_Voter::newFromId( $voterId ); |
| 78 | + $voter = SecurePoll_Voter::newFromId( $this->context, $voterId ); |
73 | 79 | if ( !$voter || $voter->getElectionId() != $election->getId() ) { |
74 | 80 | return false; |
75 | 81 | } else { |
— | — | @@ -87,7 +93,7 @@ |
88 | 94 | * @return SecurePoll_Voter |
89 | 95 | */ |
90 | 96 | function getVoter( $params ) { |
91 | | - $dbw = wfGetDB( DB_MASTER ); |
| 97 | + $dbw = $this->context->getDB(); |
92 | 98 | |
93 | 99 | # This needs to be protected by FOR UPDATE |
94 | 100 | # Otherwise a race condition could lead to duplicate users for a single remote user, |
— | — | @@ -107,10 +113,10 @@ |
108 | 114 | if ( $row ) { |
109 | 115 | # No need to hold the lock longer |
110 | 116 | $dbw->commit(); |
111 | | - $user = SecurePoll_Voter::newFromRow( $row ); |
| 117 | + $user = SecurePoll_Voter::newFromRow( $this->context, $row ); |
112 | 118 | } else { |
113 | 119 | # Lock needs to be held until the row is inserted |
114 | | - $user = SecurePoll_Voter::createVoter( $params ); |
| 120 | + $user = SecurePoll_Voter::createVoter( $this->context, $params ); |
115 | 121 | $dbw->commit(); |
116 | 122 | } |
117 | 123 | return $user; |
— | — | @@ -178,7 +184,7 @@ |
179 | 185 | if ( $wgUser->isAnon() ) { |
180 | 186 | return Status::newFatal( 'securepoll-not-logged-in' ); |
181 | 187 | } |
182 | | - $params = self::getUserParams( $wgUser ); |
| 188 | + $params = $this->getUserParams( $wgUser ); |
183 | 189 | $params['electionId'] = $election->getId(); |
184 | 190 | $qualStatus = $election->getQualifiedStatus( $params ); |
185 | 191 | if ( !$qualStatus->isOK() ) { |
— | — | @@ -193,7 +199,7 @@ |
194 | 200 | * @param $user User |
195 | 201 | * @return array |
196 | 202 | */ |
197 | | - static function getUserParams( $user ) { |
| 203 | + function getUserParams( $user ) { |
198 | 204 | global $wgServer; |
199 | 205 | return array( |
200 | 206 | 'name' => $user->getName(), |
— | — | @@ -207,7 +213,7 @@ |
208 | 214 | 'bot' => $user->isBot(), |
209 | 215 | 'language' => $user->getOption( 'language' ), |
210 | 216 | 'groups' => $user->getGroups(), |
211 | | - 'lists' => self::getLists( $user ) |
| 217 | + 'lists' => $this->getLists( $user ) |
212 | 218 | ) |
213 | 219 | ); |
214 | 220 | } |
— | — | @@ -217,8 +223,8 @@ |
218 | 224 | * @param $user User |
219 | 225 | * @return array |
220 | 226 | */ |
221 | | - static function getLists( $user ) { |
222 | | - $dbr = wfGetDB( DB_SLAVE ); |
| 227 | + function getLists( $user ) { |
| 228 | + $dbr = $this->context->getDB(); |
223 | 229 | $res = $dbr->select( |
224 | 230 | 'securepoll_lists', |
225 | 231 | array( 'li_name' ), |
Index: trunk/extensions/SecurePoll/includes/Crypt.php |
— | — | @@ -29,9 +29,9 @@ |
30 | 30 | * Create an encryption object of the given type. Currently only "gpg" is |
31 | 31 | * implemented. |
32 | 32 | */ |
33 | | - static function factory( $type, $election ) { |
| 33 | + static function factory( $context, $type, $election ) { |
34 | 34 | if ( $type === 'gpg' ) { |
35 | | - return new SecurePoll_GpgCrypt( $election ); |
| 35 | + return new SecurePoll_GpgCrypt( $context, $election ); |
36 | 36 | } else { |
37 | 37 | return false; |
38 | 38 | } |
— | — | @@ -50,14 +50,15 @@ |
51 | 51 | * gpg-decrypt-key is for tallying. |
52 | 52 | */ |
53 | 53 | class SecurePoll_GpgCrypt { |
54 | | - var $election; |
| 54 | + var $context, $election; |
55 | 55 | var $recipient, $signer, $homeDir; |
56 | 56 | |
57 | 57 | /** |
58 | 58 | * Constructor. |
59 | 59 | * @param $election SecurePoll_Election |
60 | 60 | */ |
61 | | - function __construct( $election ) { |
| 61 | + function __construct( $context, $election ) { |
| 62 | + $this->context = $context; |
62 | 63 | $this->election = $election; |
63 | 64 | } |
64 | 65 | |
Index: trunk/extensions/SecurePoll/includes/DumpPage.php |
— | — | @@ -19,7 +19,7 @@ |
20 | 20 | } |
21 | 21 | |
22 | 22 | $electionId = intval( $params[0] ); |
23 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 23 | + $this->election = $this->context->getElection( $electionId ); |
24 | 24 | if ( !$this->election ) { |
25 | 25 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
26 | 26 | return; |
— | — | @@ -66,12 +66,12 @@ |
67 | 67 | |
68 | 68 | $this->headersSent = true; |
69 | 69 | $wgOut->disable(); |
70 | | - header( 'Content-Type: text/plain' ); |
| 70 | + header( 'Content-Type: application/vnd.mediawiki.securepoll' ); |
71 | 71 | $electionId = $this->election->getId(); |
72 | | - $filename = urlencode( "SecurePoll-$electionId-" . wfTimestampNow() ); |
| 72 | + $filename = urlencode( "$electionId-" . wfTimestampNow() . '.securepoll' ); |
73 | 73 | header( "Content-Disposition: attachment; filename=$filename" ); |
74 | 74 | echo "<SecurePoll>\n<election>\n" . |
75 | 75 | $this->election->getConfXml(); |
76 | | - SecurePoll_Entity::setLanguages( array( $this->election->getLanguage() ) ); |
| 76 | + $this->context->setLanguages( array( $this->election->getLanguage() ) ); |
77 | 77 | } |
78 | 78 | } |
Index: trunk/extensions/SecurePoll/includes/Election.php |
— | — | @@ -58,31 +58,22 @@ |
59 | 59 | /** |
60 | 60 | * Constructor. |
61 | 61 | * |
62 | | - * Do not use this constructor directly, instead use SecurePoll::getElection(). |
| 62 | + * Do not use this constructor directly, instead use SecurePoll_Context::getElection(). |
63 | 63 | * |
64 | 64 | * @param $id integer |
65 | 65 | */ |
66 | | - function __construct( $id ) { |
67 | | - parent::__construct( 'election', $id ); |
| 66 | + function __construct( $context, $info ) { |
| 67 | + parent::__construct( $context, 'election', $info ); |
| 68 | + $this->title = $info['title']; |
| 69 | + $this->ballotType = $info['ballot']; |
| 70 | + $this->tallyType = $info['tally']; |
| 71 | + $this->primaryLang = $info['primaryLang']; |
| 72 | + $this->startDate = $info['startDate']; |
| 73 | + $this->endDate = $info['endDate']; |
| 74 | + $this->authType = $info['auth']; |
68 | 75 | } |
69 | 76 | |
70 | 77 | /** |
71 | | - * Create an object based on a DB result row. |
72 | | - * @param $row object |
73 | | - */ |
74 | | - static function newFromRow( $row ) { |
75 | | - $election = new self( $row->el_entity ); |
76 | | - $election->title = $row->el_title; |
77 | | - $election->ballotType = $row->el_ballot; |
78 | | - $election->tallyType = $row->el_tally; |
79 | | - $election->primaryLang = $row->el_primary_lang; |
80 | | - $election->startDate = $row->el_start_date; |
81 | | - $election->endDate = $row->el_end_date; |
82 | | - $election->authType = $row->el_auth_type; |
83 | | - return $election; |
84 | | - } |
85 | | - |
86 | | - /** |
87 | 78 | * Get a list of localisable message names. See SecurePoll_Entity. |
88 | 79 | */ |
89 | 80 | function getMessageNames() { |
— | — | @@ -134,7 +125,7 @@ |
135 | 126 | */ |
136 | 127 | function getBallot() { |
137 | 128 | if ( !$this->ballot ) { |
138 | | - $this->ballot = SecurePoll_Ballot::factory( $this->ballotType, $this ); |
| 129 | + $this->ballot = $this->context->newBallot( $this->ballotType, $this ); |
139 | 130 | } |
140 | 131 | return $this->ballot; |
141 | 132 | } |
— | — | @@ -190,7 +181,7 @@ |
191 | 182 | * Returns true if the user is an admin of the current election. |
192 | 183 | * @param $user User |
193 | 184 | */ |
194 | | - function isAdmin( User $user ) { |
| 185 | + function isAdmin( $user ) { |
195 | 186 | $admins = array_map( 'trim', explode( '|', $this->getProperty( 'admins' ) ) ); |
196 | 187 | return in_array( $user->getName(), $admins ); |
197 | 188 | } |
— | — | @@ -200,7 +191,7 @@ |
201 | 192 | * @param $voter SecurePoll_Voter |
202 | 193 | */ |
203 | 194 | function hasVoted( $voter ) { |
204 | | - $db = wfGetDB( DB_MASTER ); |
| 195 | + $db = $this->context->getDB(); |
205 | 196 | $row = $db->selectRow( |
206 | 197 | 'securepoll_votes', |
207 | 198 | array( "1" ), |
— | — | @@ -227,33 +218,11 @@ |
228 | 219 | */ |
229 | 220 | function getQuestions() { |
230 | 221 | if ( $this->questions === null ) { |
231 | | - $db = wfGetDB( DB_MASTER ); |
232 | | - $res = $db->select( |
233 | | - array( 'securepoll_questions', 'securepoll_options' ), |
234 | | - '*', |
235 | | - array( |
236 | | - 'qu_election' => $this->getId(), |
237 | | - 'op_question=qu_entity' |
238 | | - ), |
239 | | - __METHOD__, |
240 | | - array( 'ORDER BY' => 'qu_index, qu_entity' ) |
241 | | - ); |
242 | | - |
| 222 | + $info = $this->context->getStore()->getQuestionInfo( $this->getId() ); |
243 | 223 | $this->questions = array(); |
244 | | - $options = array(); |
245 | | - $questionId = false; |
246 | | - foreach ( $res as $row ) { |
247 | | - if ( $questionId === false ) { |
248 | | - } elseif ( $questionId !== $row->qu_entity ) { |
249 | | - $this->questions[] = new SecurePoll_Question( $questionId, $options ); |
250 | | - $options = array(); |
251 | | - } |
252 | | - $options[] = SecurePoll_Option::newFromRow( $row ); |
253 | | - $questionId = $row->qu_entity; |
| 224 | + foreach ( $info as $questionInfo ) { |
| 225 | + $this->questions[] = $this->context->newQuestion( $questionInfo ); |
254 | 226 | } |
255 | | - if ( $questionId !== false ) { |
256 | | - $this->questions[] = new SecurePoll_Question( $questionId, $options ); |
257 | | - } |
258 | 227 | } |
259 | 228 | return $this->questions; |
260 | 229 | } |
— | — | @@ -264,7 +233,7 @@ |
265 | 234 | */ |
266 | 235 | function getAuth() { |
267 | 236 | if ( !$this->auth ) { |
268 | | - $this->auth = SecurePoll_Auth::factory( $this->authType ); |
| 237 | + $this->auth = $this->context->newAuth( $this->authType ); |
269 | 238 | } |
270 | 239 | return $this->auth; |
271 | 240 | } |
— | — | @@ -288,7 +257,7 @@ |
289 | 258 | if ( $type === false || $type === 'none' ) { |
290 | 259 | return false; |
291 | 260 | } |
292 | | - $crypt = SecurePoll_Crypt::factory( $type, $this ); |
| 261 | + $crypt = $this->context->newCrypt( $type, $this ); |
293 | 262 | if ( !$crypt ) { |
294 | 263 | throw new MWException( 'Invalid encryption type' ); |
295 | 264 | } |
— | — | @@ -296,19 +265,10 @@ |
297 | 266 | } |
298 | 267 | |
299 | 268 | /** |
300 | | - * Get the tallier objects |
301 | | - * @return SecurePoll_Tallier |
| 269 | + * Get the tally type |
302 | 270 | */ |
303 | | - function getTalliers() { |
304 | | - $talliers = array(); |
305 | | - foreach ( $this->getQuestions() as $question ) { |
306 | | - $tallier = SecurePoll_Tallier::factory( $this->tallyType, $question ); |
307 | | - if ( !$tallier ) { |
308 | | - throw new MWException( 'Invalid tally type' ); |
309 | | - } |
310 | | - $talliers[$question->getId()] = $tallier; |
311 | | - } |
312 | | - return $talliers; |
| 271 | + function getTallyType() { |
| 272 | + return $this->tallyType; |
313 | 273 | } |
314 | 274 | |
315 | 275 | /** |
— | — | @@ -319,12 +279,12 @@ |
320 | 280 | return Status::newFatal( 'securepoll-dump-no-crypt' ); |
321 | 281 | } |
322 | 282 | |
323 | | - $random = SecurePoll::getRandom(); |
| 283 | + $random = $this->context->getRandom(); |
324 | 284 | $status = $random->open(); |
325 | 285 | if ( !$status->isOK() ) { |
326 | 286 | return $status; |
327 | 287 | } |
328 | | - $db = wfGetDB( DB_SLAVE ); |
| 288 | + $db = $this->context->getDB(); |
329 | 289 | $res = $db->select( |
330 | 290 | 'securepoll_votes', |
331 | 291 | array( '*' ), |
— | — | @@ -354,7 +314,7 @@ |
355 | 315 | Xml::element( 'title', array(), $this->title ) . "\n" . |
356 | 316 | Xml::element( 'ballot', array(), $this->ballotType ) . "\n" . |
357 | 317 | Xml::element( 'tally', array(), $this->tallyType ) . "\n" . |
358 | | - Xml::element( 'lang', array(), $this->primaryLang ) . "\n" . |
| 318 | + Xml::element( 'primaryLang', array(), $this->primaryLang ) . "\n" . |
359 | 319 | Xml::element( 'startDate', array(), wfTimestamp( TS_ISO_8601, $this->startDate ) ) . "\n" . |
360 | 320 | Xml::element( 'endDate', array(), wfTimestamp( TS_ISO_8601, $this->endDate ) ) . "\n" . |
361 | 321 | Xml::element( 'auth', array(), $this->authType ) . "\n" . |
— | — | @@ -365,5 +325,20 @@ |
366 | 326 | $s .= "</configuration>\n"; |
367 | 327 | return $s; |
368 | 328 | } |
| 329 | + |
| 330 | + /** |
| 331 | + * Tally the valid votes for this election. |
| 332 | + * Returns a Status object. On success, the value property will contain a |
| 333 | + * SecurePoll_ElectionTallier object. |
| 334 | + */ |
| 335 | + function tally() { |
| 336 | + $tallier = $this->context->newElectionTallier( $this ); |
| 337 | + $status = $tallier->execute(); |
| 338 | + if ( $status->isOK() ) { |
| 339 | + return Status::newGood( $tallier ); |
| 340 | + } else { |
| 341 | + return $status; |
| 342 | + } |
| 343 | + } |
369 | 344 | } |
370 | 345 | |
Index: trunk/extensions/SecurePoll/includes/LoginPage.php |
— | — | @@ -14,7 +14,7 @@ |
15 | 15 | } |
16 | 16 | |
17 | 17 | $electionId = intval( $params[0] ); |
18 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 18 | + $this->election = $this->context->getElection( $electionId ); |
19 | 19 | if ( !$this->election ) { |
20 | 20 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
21 | 21 | return; |
Index: trunk/extensions/SecurePoll/includes/MessageDumpPage.php |
— | — | @@ -10,7 +10,7 @@ |
11 | 11 | } |
12 | 12 | |
13 | 13 | $electionId = intval( $params[0] ); |
14 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 14 | + $this->election = $this->context->getElection( $electionId ); |
15 | 15 | if ( !$this->election ) { |
16 | 16 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
17 | 17 | return; |
— | — | @@ -20,7 +20,7 @@ |
21 | 21 | header( 'Content-Type: application/x-sql; charset=utf-8' ); |
22 | 22 | $filename = urlencode( "sp-msgs-$electionId-" . wfTimestampNow() . '.sql' ); |
23 | 23 | header( "Content-Disposition: attachment; filename=$filename" ); |
24 | | - $dbr = wfGetDB( DB_SLAVE ); |
| 24 | + $dbr = $this->context->getDB(); |
25 | 25 | |
26 | 26 | $entities = array_merge( array( $this->election ), $this->election->getDescendants() ); |
27 | 27 | $ids = array(); |
Index: trunk/extensions/SecurePoll/includes/Question.php |
— | — | @@ -9,12 +9,15 @@ |
10 | 10 | |
11 | 11 | /** |
12 | 12 | * Constructor |
13 | | - * @param $id integer |
14 | | - * @param $options array Array of SecurePoll_Option children |
| 13 | + * @param $context SecurePoll_Context |
| 14 | + * @param $info Associative array of entity info |
15 | 15 | */ |
16 | | - function __construct( $id, $options ) { |
17 | | - parent::__construct( 'question', $id ); |
18 | | - $this->options = $options; |
| 16 | + function __construct( $context, $info ) { |
| 17 | + parent::__construct( $context, 'question', $info ); |
| 18 | + $this->options = array(); |
| 19 | + foreach ( $info['options'] as $optionInfo ) { |
| 20 | + $this->options[] = new SecurePoll_Option( $context, $optionInfo ); |
| 21 | + } |
19 | 22 | } |
20 | 23 | |
21 | 24 | /** |
Index: trunk/extensions/SecurePoll/includes/Option.php |
— | — | @@ -6,22 +6,15 @@ |
7 | 7 | */ |
8 | 8 | class SecurePoll_Option extends SecurePoll_Entity { |
9 | 9 | /** |
10 | | - * Create a new option from a DB row |
11 | | - * @param $row object |
| 10 | + * Constructor |
| 11 | + * @param $context SecurePoll_Context |
| 12 | + * @param $info Associative array of entity info |
12 | 13 | */ |
13 | | - static function newFromRow( $row ) { |
14 | | - return new self( $row->op_entity ); |
| 14 | + function __construct( $context, $info ) { |
| 15 | + parent::__construct( $context, 'option', $info ); |
15 | 16 | } |
16 | 17 | |
17 | 18 | /** |
18 | | - * Constructor, from entity ID |
19 | | - * @param $id integer |
20 | | - */ |
21 | | - function __construct( $id ) { |
22 | | - parent::__construct( 'option', $id ); |
23 | | - } |
24 | | - |
25 | | - /** |
26 | 19 | * Get a list of localisable message names. This is used to provide the |
27 | 20 | * translate subpage with a list of messages to localise. |
28 | 21 | */ |
Index: trunk/extensions/SecurePoll/includes/ListPage.php |
— | — | @@ -20,7 +20,7 @@ |
21 | 21 | } |
22 | 22 | |
23 | 23 | $electionId = intval( $params[0] ); |
24 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 24 | + $this->election = $this->context->getElection( $electionId ); |
25 | 25 | if ( !$this->election ) { |
26 | 26 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
27 | 27 | return; |
— | — | @@ -84,7 +84,7 @@ |
85 | 85 | */ |
86 | 86 | static function ajaxStrike( $action, $id, $reason ) { |
87 | 87 | wfLoadExtensionMessages( 'SecurePoll' ); |
88 | | - $db = wfGetDB( DB_MASTER ); |
| 88 | + $db = $this->context->getDB(); |
89 | 89 | $table = $db->tableName( 'securepoll_elections' ); |
90 | 90 | $row = $db->selectRow( |
91 | 91 | array( 'securepoll_votes', 'securepoll_elections' ), |
— | — | @@ -98,9 +98,9 @@ |
99 | 99 | 'message' => wfMsgHtml( 'securepoll-strike-nonexistent' ) |
100 | 100 | ) ); |
101 | 101 | } |
102 | | - $page = new SecurePollPage; |
| 102 | + $page = new SecurePoll_BasePage; |
103 | 103 | $subpage = new self( $page ); |
104 | | - $subpage->election = SecurePoll_Election::newFromRow( $row ); |
| 104 | + $subpage->election = $subpage->context->newElectionFromRow( $row ); |
105 | 105 | $status = $subpage->strike( $action, $id, $reason ); |
106 | 106 | if ( $status->isGood() ) { |
107 | 107 | return Xml::encodeJsVar( (object)array( 'status' => 'good' ) ); |
— | — | @@ -122,7 +122,7 @@ |
123 | 123 | */ |
124 | 124 | function strike( $action, $voteId, $reason ) { |
125 | 125 | global $wgUser; |
126 | | - $dbw = wfGetDB( DB_MASTER ); |
| 126 | + $dbw = $this->context->getDB(); |
127 | 127 | if ( !$this->election->isAdmin( $wgUser ) ) { |
128 | 128 | return Status::newFatal( 'securepoll-need-admin' ); |
129 | 129 | } |
Index: trunk/extensions/SecurePoll/includes/Base.php |
— | — | @@ -1,36 +1,5 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | | -class SecurePoll { |
5 | | - static $random; |
6 | | - |
7 | | - static function getElection( $id ) { |
8 | | - $db = wfGetDB( DB_MASTER ); |
9 | | - $row = $db->selectRow( 'securepoll_elections', '*', array( 'el_entity' => $id ), __METHOD__ ); |
10 | | - if ( $row ) { |
11 | | - return SecurePoll_Election::newFromRow( $row ); |
12 | | - } else { |
13 | | - return false; |
14 | | - } |
15 | | - } |
16 | | - |
17 | | - static function getElectionByTitle( $name ) { |
18 | | - $db = wfGetDB( DB_MASTER ); |
19 | | - $row = $db->selectRow( 'securepoll_elections', '*', array( 'el_title' => $name ), __METHOD__ ); |
20 | | - if ( $row ) { |
21 | | - return SecurePoll_Election::newFromRow( $row ); |
22 | | - } else { |
23 | | - return false; |
24 | | - } |
25 | | - } |
26 | | - |
27 | | - static function getRandom() { |
28 | | - if ( !self::$random ) { |
29 | | - self::$random = new SecurePoll_Random; |
30 | | - } |
31 | | - return self::$random; |
32 | | - } |
33 | | -} |
34 | | - |
35 | 4 | class SecurePoll_BasePage extends UnlistedSpecialPage { |
36 | 5 | static $pages = array( |
37 | 6 | 'details' => 'SecurePoll_DetailsPage', |
— | — | @@ -44,11 +13,14 @@ |
45 | 14 | 'vote' => 'SecurePoll_VotePage', |
46 | 15 | ); |
47 | 16 | |
| 17 | + var $sp_context; |
| 18 | + |
48 | 19 | /** |
49 | 20 | * Constructor |
50 | 21 | */ |
51 | 22 | public function __construct() { |
52 | 23 | parent::__construct( 'SecurePoll' ); |
| 24 | + $this->sp_context = new SecurePoll_Context; |
53 | 25 | } |
54 | 26 | |
55 | 27 | /** |
— | — | @@ -57,7 +29,7 @@ |
58 | 30 | * @param $paramString Mixed: parameter passed to the page or null |
59 | 31 | */ |
60 | 32 | public function execute( $paramString ) { |
61 | | - global $wgOut, $wgRequest, $wgScriptPath; |
| 33 | + global $wgOut, $wgUser, $wgRequest, $wgScriptPath; |
62 | 34 | |
63 | 35 | wfLoadExtensionMessages( 'SecurePoll' ); |
64 | 36 | |
— | — | @@ -83,9 +55,16 @@ |
84 | 56 | return; |
85 | 57 | } |
86 | 58 | |
| 59 | + if ( !($page instanceof SecurePoll_EntryPage ) ) { |
| 60 | + $this->setSubtitle(); |
| 61 | + } |
| 62 | + |
87 | 63 | $page->execute( $params ); |
88 | 64 | } |
89 | 65 | |
| 66 | + /** |
| 67 | + * Get a SecurePoll_Page subclass object for the given subpage name |
| 68 | + */ |
90 | 69 | function getSubpage( $name ) { |
91 | 70 | if ( !isset( self::$pages[$name] ) ) { |
92 | 71 | return false; |
— | — | @@ -95,10 +74,32 @@ |
96 | 75 | return $page; |
97 | 76 | } |
98 | 77 | |
| 78 | + /** |
| 79 | + * Get a random token for CSRF protection |
| 80 | + */ |
99 | 81 | function getEditToken() { |
100 | 82 | if ( !isset( $_SESSION['spToken'] ) ) { |
101 | 83 | $_SESSION['spToken'] = sha1( mt_rand() . mt_rand() . mt_rand() ); |
102 | 84 | } |
103 | 85 | return $_SESSION['spToken']; |
104 | 86 | } |
| 87 | + |
| 88 | + /** |
| 89 | + * Set a navigation subtitle. |
| 90 | + * Each argument is a two-element array giving a Title object to be used as |
| 91 | + * a link target, and the link text. |
| 92 | + */ |
| 93 | + function setSubtitle( /*...*/ ) { |
| 94 | + global $wgUser, $wgOut; |
| 95 | + $skin = $wgUser->getSkin(); |
| 96 | + $title = $this->getTitle(); |
| 97 | + $subtitle = '< ' . $skin->linkKnown( $title, htmlspecialchars( $title->getText() ) ); |
| 98 | + $pipe = wfMsg( 'pipe-separator' ); |
| 99 | + $links = func_get_args(); |
| 100 | + foreach ( $links as $link ) { |
| 101 | + list( $title, $text ) = $link; |
| 102 | + $subtitle .= $pipe . $skin->linkKnown( $title, htmlspecialchars( $text ) ); |
| 103 | + } |
| 104 | + $wgOut->setSubtitle( $subtitle ); |
| 105 | + } |
105 | 106 | } |
Index: trunk/extensions/SecurePoll/includes/VotePage.php |
— | — | @@ -19,7 +19,7 @@ |
20 | 20 | } |
21 | 21 | |
22 | 22 | $electionId = intval( $params[0] ); |
23 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 23 | + $this->election = $this->context->getElection( $electionId ); |
24 | 24 | if ( !$this->election ) { |
25 | 25 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
26 | 26 | return; |
— | — | @@ -160,7 +160,7 @@ |
161 | 161 | $encrypted = $status->value; |
162 | 162 | } |
163 | 163 | |
164 | | - $dbw = wfGetDB( DB_MASTER ); |
| 164 | + $dbw = $this->context->getDB(); |
165 | 165 | $dbw->begin(); |
166 | 166 | |
167 | 167 | # Mark previous votes as old |
Index: trunk/extensions/SecurePoll/includes/Page.php |
— | — | @@ -5,6 +5,7 @@ |
6 | 6 | */ |
7 | 7 | abstract class SecurePoll_Page { |
8 | 8 | var $parent, $election, $auth, $user; |
| 9 | + var $context; |
9 | 10 | |
10 | 11 | /** |
11 | 12 | * Constructor. |
— | — | @@ -12,6 +13,7 @@ |
13 | 14 | */ |
14 | 15 | function __construct( $parent ) { |
15 | 16 | $this->parent = $parent; |
| 17 | + $this->context = $parent->sp_context; |
16 | 18 | } |
17 | 19 | |
18 | 20 | /** |
— | — | @@ -48,6 +50,6 @@ |
49 | 51 | if ( $fallback != 'en' ) { |
50 | 52 | $languages[] = 'en'; |
51 | 53 | } |
52 | | - SecurePoll_Entity::setLanguages( $languages ); |
| 54 | + $this->context->setLanguages( $languages ); |
53 | 55 | } |
54 | 56 | } |
Index: trunk/extensions/SecurePoll/includes/Entity.php |
— | — | @@ -12,22 +12,21 @@ |
13 | 13 | */ |
14 | 14 | class SecurePoll_Entity { |
15 | 15 | var $id; |
| 16 | + var $context; |
16 | 17 | var $messagesLoaded = array(); |
17 | 18 | var $properties; |
18 | 19 | |
19 | | - static $languages = array( 'en' ); |
20 | | - static $messageCache = array(); |
21 | | - static $parserOptions; |
22 | | - |
23 | 20 | /** |
24 | 21 | * Create an entity of the given type. This is typically called from the |
25 | 22 | * child constructor. |
| 23 | + * @param $context SecurePoll_Context |
26 | 24 | * @param $type string |
27 | | - * @param $id integer |
| 25 | + * @param $info Associative array of entity info |
28 | 26 | */ |
29 | | - function __construct( $type, $id = false ) { |
| 27 | + function __construct( $context, $type, $info ) { |
| 28 | + $this->context = $context; |
30 | 29 | $this->type = $type; |
31 | | - $this->id = $id; |
| 30 | + $this->id = isset( $info['id'] ) ? $info['id'] : false; |
32 | 31 | } |
33 | 32 | |
34 | 33 | /** |
— | — | @@ -55,17 +54,6 @@ |
56 | 55 | } |
57 | 56 | |
58 | 57 | /** |
59 | | - * Set the global language fallback sequence. |
60 | | - * |
61 | | - * @param $languages array A list of language codes. When a message is |
62 | | - * requested, the first code in the array will be tried first, followed |
63 | | - * by the subsequent codes. |
64 | | - */ |
65 | | - static function setLanguages( $languages ) { |
66 | | - self::$languages = $languages; |
67 | | - } |
68 | | - |
69 | | - /** |
70 | 58 | * Get the child entity objects. When the messages of an object are loaded, |
71 | 59 | * the messages of the children are loaded automatically, to reduce the |
72 | 60 | * query count. |
— | — | @@ -97,32 +85,13 @@ |
98 | 86 | */ |
99 | 87 | function loadMessages( $lang = false ) { |
100 | 88 | if ( $lang === false ) { |
101 | | - $lang = reset( self::$languages ); |
| 89 | + $lang = reset( $this->context->languages ); |
102 | 90 | } |
103 | 91 | $ids = array( $this->getId() ); |
104 | 92 | foreach ( $this->getDescendants() as $child ) { |
105 | | - $id = $child->getId(); |
106 | | - if ( !isset( self::$messageCache[$lang][$id] ) ) { |
107 | | - $ids[] = $id; |
108 | | - } |
| 93 | + $ids[] = $child->getId(); |
109 | 94 | } |
110 | | - if ( !count( $ids ) ) { |
111 | | - return; |
112 | | - } |
113 | | - |
114 | | - $db = wfGetDB( DB_MASTER ); |
115 | | - $res = $db->select( |
116 | | - 'securepoll_msgs', |
117 | | - '*', |
118 | | - array( |
119 | | - 'msg_entity' => $ids, |
120 | | - 'msg_lang' => $lang |
121 | | - ), |
122 | | - __METHOD__ |
123 | | - ); |
124 | | - foreach ( $res as $row ) { |
125 | | - self::$messageCache[$row->msg_lang][$row->msg_entity][$row->msg_key] = $row->msg_text; |
126 | | - } |
| 95 | + $this->context->getMessages( $lang, $ids ); |
127 | 96 | $this->messagesLoaded[$lang] = true; |
128 | 97 | } |
129 | 98 | |
— | — | @@ -132,15 +101,11 @@ |
133 | 102 | * automatically. |
134 | 103 | */ |
135 | 104 | function loadProperties() { |
136 | | - $db = wfGetDB( DB_MASTER ); |
137 | | - $res = $db->select( |
138 | | - 'securepoll_properties', |
139 | | - '*', |
140 | | - array( 'pr_entity' => $this->getId() ), |
141 | | - __METHOD__ ); |
142 | | - $this->properties = array(); |
143 | | - foreach ( $res as $row ) { |
144 | | - $this->properties[$row->pr_key] = $row->pr_value; |
| 105 | + $properties = $this->context->getStore()->getProperties( array( $this->getId() ) ); |
| 106 | + if ( count( $properties ) ) { |
| 107 | + $this->properties = reset( $properties ); |
| 108 | + } else { |
| 109 | + $this->properties = array(); |
145 | 110 | } |
146 | 111 | } |
147 | 112 | |
— | — | @@ -155,11 +120,7 @@ |
156 | 121 | if ( empty( $this->messagesLoaded[$language] ) ) { |
157 | 122 | $this->loadMessages( $language ); |
158 | 123 | } |
159 | | - if ( !isset( self::$messageCache[$language][$this->getId()][$name] ) ) { |
160 | | - return false; |
161 | | - } else { |
162 | | - return self::$messageCache[$language][$this->getId()][$name]; |
163 | | - } |
| 124 | + return $this->context->getMessage( $language, $this->getId(), $name ); |
164 | 125 | } |
165 | 126 | |
166 | 127 | /** |
— | — | @@ -171,12 +132,13 @@ |
172 | 133 | */ |
173 | 134 | function getMessage( $name ) { |
174 | 135 | $id = $this->getId(); |
175 | | - foreach ( self::$languages as $language ) { |
| 136 | + foreach ( $this->context->languages as $language ) { |
176 | 137 | if ( empty( $this->messagesLoaded[$language] ) ) { |
177 | 138 | $this->loadMessages( $language ); |
178 | 139 | } |
179 | | - if ( isset( self::$messageCache[$language][$id][$name] ) ) { |
180 | | - return self::$messageCache[$language][$id][$name]; |
| 140 | + $message = $this->getRawMessage( $name, $language ); |
| 141 | + if ( $message !== false ) { |
| 142 | + return $message; |
181 | 143 | } |
182 | 144 | } |
183 | 145 | return "[$name]"; |
— | — | @@ -187,16 +149,14 @@ |
188 | 150 | */ |
189 | 151 | function parseMessage( $name, $lineStart = true ) { |
190 | 152 | global $wgParser, $wgTitle; |
191 | | - if ( !self::$parserOptions ) { |
192 | | - self::$parserOptions = new ParserOptions; |
193 | | - } |
| 153 | + $parserOptions = $this->context->getParserOptions(); |
194 | 154 | if ( $wgTitle ) { |
195 | 155 | $title = $wgTitle; |
196 | 156 | } else { |
197 | 157 | $title = SpecialPage::getTitleFor( 'SecurePoll' ); |
198 | 158 | } |
199 | 159 | $wikiText = $this->getMessage( $name ); |
200 | | - $out = $wgParser->parse( $wikiText, $title, self::$parserOptions, $lineStart ); |
| 160 | + $out = $wgParser->parse( $wikiText, $title, $parserOptions, $lineStart ); |
201 | 161 | return $out->getText(); |
202 | 162 | } |
203 | 163 | |
— | — | @@ -252,7 +212,7 @@ |
253 | 213 | $s .= Xml::element( 'property', array( 'name' => $name ), $value ) . "\n"; |
254 | 214 | } |
255 | 215 | foreach ( $this->getMessageNames() as $name ) { |
256 | | - foreach ( self::$languages as $lang ) { |
| 216 | + foreach ( $this->context->languages as $lang ) { |
257 | 217 | $s .= Xml::element( 'message', array( 'name' => $name, 'lang' => $lang ), |
258 | 218 | $this->getRawMessage( $name, $lang ) ) . "\n"; |
259 | 219 | } |
Index: trunk/extensions/SecurePoll/includes/Ballot.php |
— | — | @@ -4,7 +4,7 @@ |
5 | 5 | * Parent class for ballot forms. This is the UI component of a voting method. |
6 | 6 | */ |
7 | 7 | abstract class SecurePoll_Ballot { |
8 | | - var $election; |
| 8 | + var $election, $context; |
9 | 9 | |
10 | 10 | /** |
11 | 11 | * Get a list of names of tallying methods, which may be used to produce a |
— | — | @@ -35,17 +35,18 @@ |
36 | 36 | |
37 | 37 | /** |
38 | 38 | * Create a ballot of the given type |
| 39 | + * @param $context SecurePoll_Context |
39 | 40 | * @param $type string |
40 | 41 | * @param $election SecurePoll_Election |
41 | 42 | */ |
42 | | - static function factory( $type, $election ) { |
| 43 | + static function factory( $context, $type, $election ) { |
43 | 44 | switch ( $type ) { |
44 | 45 | case 'approval': |
45 | | - return new SecurePoll_ApprovalBallot( $election ); |
| 46 | + return new SecurePoll_ApprovalBallot( $context, $election ); |
46 | 47 | case 'preferential': |
47 | | - return new SecurePoll_PreferentialBallot( $election ); |
| 48 | + return new SecurePoll_PreferentialBallot( $context, $election ); |
48 | 49 | case 'choose': |
49 | | - return new SecurePoll_ChooseBallot( $election ); |
| 50 | + return new SecurePoll_ChooseBallot( $context, $election ); |
50 | 51 | default: |
51 | 52 | throw new MWException( "Invalid ballot type: $type" ); |
52 | 53 | } |
— | — | @@ -53,9 +54,11 @@ |
54 | 55 | |
55 | 56 | /** |
56 | 57 | * Constructor. |
| 58 | + * @param $context SecurePoll_Context |
57 | 59 | * @param $election SecurePoll_Election |
58 | 60 | */ |
59 | | - function __construct( $election ) { |
| 61 | + function __construct( $context, $election ) { |
| 62 | + $this->context = $context; |
60 | 63 | $this->election = $election; |
61 | 64 | } |
62 | 65 | |
— | — | @@ -116,10 +119,12 @@ |
117 | 120 | $optionHTML = $option->parseMessageInline( 'text' ); |
118 | 121 | $optionId = $option->getId(); |
119 | 122 | $radioId = "{$name}_opt{$optionId}"; |
120 | | - $s .= Xml::radio( $name, $optionId, false, array( 'id' => $radioId ) ) . |
| 123 | + $s .= |
| 124 | + '<div class="securepoll-option-choose">' . |
| 125 | + Xml::radio( $name, $optionId, false, array( 'id' => $radioId ) ) . |
121 | 126 | ' ' . |
122 | 127 | Xml::tags( 'label', array( 'for' => $radioId ), $optionHTML ) . |
123 | | - "<br/>\n"; |
| 128 | + "</div>\n"; |
124 | 129 | } |
125 | 130 | return $s; |
126 | 131 | } |
— | — | @@ -189,6 +194,7 @@ |
190 | 195 | $inputId = "{$name}_opt{$optionId}"; |
191 | 196 | $oldValue = $wgRequest->getVal( $inputId, '' ); |
192 | 197 | $s .= |
| 198 | + '<div class="securepoll-option-preferential">' . |
193 | 199 | Xml::input( $inputId, '3', $oldValue, array( |
194 | 200 | 'id' => $inputId, |
195 | 201 | 'maxlength' => 3, |
— | — | @@ -196,7 +202,7 @@ |
197 | 203 | ' ' . |
198 | 204 | Xml::tags( 'label', array( 'for' => $inputId ), $optionHTML ) . |
199 | 205 | ' ' . |
200 | | - "<br/>\n"; |
| 206 | + "</div>\n"; |
201 | 207 | } |
202 | 208 | return $s; |
203 | 209 | } |
Index: trunk/extensions/SecurePoll/includes/ElectionTallier.php |
— | — | @@ -0,0 +1,119 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * A helper class for tallying a whole election (with multiple questions). |
| 6 | + * Most of the functionality is contained in the SecurePoll_Tallier subclasses |
| 7 | + * which operate on a single question at a time. |
| 8 | + * |
| 9 | + * A convenience function for accessing this class is |
| 10 | + * SecurePoll_Election::tally(). |
| 11 | + */ |
| 12 | +class SecurePoll_ElectionTallier { |
| 13 | + /** |
| 14 | + * Constructor. |
| 15 | + * @param $context SecurePoll_Context |
| 16 | + * @param $election SecurePoll_Election |
| 17 | + */ |
| 18 | + function __construct( $context, $election ) { |
| 19 | + $this->context = $context; |
| 20 | + $this->election = $election; |
| 21 | + } |
| 22 | + |
| 23 | + /** |
| 24 | + * Do the tally. Returns a Status object. On success, the value property |
| 25 | + * of the status will be an array of SecurePoll_Tallier objects, which can |
| 26 | + * be queried for results information. |
| 27 | + */ |
| 28 | + function execute() { |
| 29 | + $store = $this->context->getStore(); |
| 30 | + $this->crypt = $this->election->getCrypt(); |
| 31 | + $this->ballot = $this->election->getBallot(); |
| 32 | + $questions = $this->election->getQuestions(); |
| 33 | + $this->talliers = array(); |
| 34 | + $tallyType = $this->election->getTallyType(); |
| 35 | + foreach ( $questions as $question ) { |
| 36 | + $tallier = $this->context->newTallier( $tallyType, $question ); |
| 37 | + if ( !$tallier ) { |
| 38 | + throw new MWException( 'Invalid tally type' ); |
| 39 | + } |
| 40 | + $this->talliers[$question->getId()] = $tallier; |
| 41 | + } |
| 42 | + |
| 43 | + $status = $store->callbackValidVotes( $this->election->getId(), array( $this, 'addRecord' ) ); |
| 44 | + if ( !$status->isOK() ) { |
| 45 | + return $status; |
| 46 | + } |
| 47 | + |
| 48 | + foreach ( $this->talliers as $tallier ) { |
| 49 | + $tallier->finishTally(); |
| 50 | + } |
| 51 | + return Status::newGood( $this->talliers ); |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Add a record. This is the callback function for SecurePoll_Store::callbackValidVotes(). |
| 56 | + * On error, the Status object returned here will be passed through back to |
| 57 | + * the caller of callbackValidVotes(). |
| 58 | + * |
| 59 | + * @param $store SecurePoll_Store |
| 60 | + * @param $record string Encrypted, packed record. |
| 61 | + * @return Status |
| 62 | + */ |
| 63 | + function addRecord( $store, $record ) { |
| 64 | + # Decrypt and unpack |
| 65 | + if ( $this->crypt ) { |
| 66 | + $status = $this->crypt->decrypt( $record ); |
| 67 | + if ( !$status->isOK() ) { |
| 68 | + return $status; |
| 69 | + } |
| 70 | + $record = $status->value; |
| 71 | + } |
| 72 | + $record = rtrim( $record ); |
| 73 | + $scores = $this->ballot->unpackRecord( $record ); |
| 74 | + |
| 75 | + # Add the record to the underlying question-specific tallier objects |
| 76 | + foreach ( $this->election->getQuestions() as $question ) { |
| 77 | + $qid = $question->getId(); |
| 78 | + if ( !isset( $scores[$qid] ) ) { |
| 79 | + return Status::newFatal( 'securepoll-tally-error' ); |
| 80 | + } |
| 81 | + if ( !$this->talliers[$qid]->addVote( $scores[$qid] ) ) { |
| 82 | + return Status::newFatal( 'securepoll-tally-error' ); |
| 83 | + } |
| 84 | + } |
| 85 | + return Status::newGood(); |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * Get HTML formatted results for this tally. Should only be called after |
| 90 | + * execute(). |
| 91 | + */ |
| 92 | + function getHtmlResult() { |
| 93 | + $s = ''; |
| 94 | + foreach ( $this->election->getQuestions() as $question ) { |
| 95 | + if ( $s !== '' ) { |
| 96 | + $s .= "<hr/>\n"; |
| 97 | + } |
| 98 | + $tallier = $this->talliers[$question->getId()]; |
| 99 | + $s .= $tallier->getHtmlResult(); |
| 100 | + } |
| 101 | + return $s; |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Get text formatted results for this tally. Should only be called after |
| 106 | + * execute(). |
| 107 | + */ |
| 108 | + function getTextResult() { |
| 109 | + $s = ''; |
| 110 | + foreach ( $this->election->getQuestions() as $question ) { |
| 111 | + if ( $s !== '' ) { |
| 112 | + $s .= "\n"; |
| 113 | + } |
| 114 | + $tallier = $this->talliers[$question->getId()]; |
| 115 | + $s .= $tallier->getTextResult(); |
| 116 | + } |
| 117 | + return $s; |
| 118 | + } |
| 119 | +} |
| 120 | + |
Property changes on: trunk/extensions/SecurePoll/includes/ElectionTallier.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 121 | + native |
Index: trunk/extensions/SecurePoll/includes/TallyPage.php |
— | — | @@ -17,14 +17,13 @@ |
18 | 18 | } |
19 | 19 | |
20 | 20 | $electionId = intval( $params[0] ); |
21 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 21 | + $this->election = $this->context->getElection( $electionId ); |
22 | 22 | if ( !$this->election ) { |
23 | 23 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
24 | 24 | return; |
25 | 25 | } |
26 | 26 | $this->initLanguage( $wgUser, $this->election ); |
27 | 27 | $wgOut->setPageTitle( wfMsg( 'securepoll-tally-title', $this->election->getMessage( 'title' ) ) ); |
28 | | - |
29 | 28 | if ( !$this->election->isAdmin( $wgUser ) ) { |
30 | 29 | $wgOut->addWikiMsg( 'securepoll-need-admin' ); |
31 | 30 | return; |
— | — | @@ -125,57 +124,13 @@ |
126 | 125 | */ |
127 | 126 | function submitLocal() { |
128 | 127 | global $wgOut; |
129 | | - $dbr = wfGetDB( DB_SLAVE ); |
130 | | - $res = $dbr->select( |
131 | | - 'securepoll_votes', |
132 | | - array( 'vote_record' ), |
133 | | - array( |
134 | | - 'vote_election' => $this->election->getId(), |
135 | | - 'vote_current' => 1, |
136 | | - 'vote_struck' => 0 |
137 | | - ), __METHOD__ |
138 | | - ); |
139 | | - $crypt = $this->election->getCrypt(); |
140 | | - $ballot = $this->election->getBallot(); |
141 | | - $questions = $this->election->getQuestions(); |
142 | | - $talliers = $this->election->getTalliers(); |
143 | | - foreach ( $res as $row ) { |
144 | | - $record = $row->vote_record; |
145 | | - if ( $crypt ) { |
146 | | - $status = $crypt->decrypt( $record ); |
147 | | - if ( !$status->isOK() ) { |
148 | | - $wgOut->addWikiText( $status->getWikiText() ); |
149 | | - return; |
150 | | - } |
151 | | - $record = $status->value; |
152 | | - } |
153 | | - $record = rtrim( $record ); |
154 | | - $scores = $ballot->unpackRecord( $record ); |
155 | | - foreach ( $questions as $question ) { |
156 | | - $qid = $question->getId(); |
157 | | - if ( !isset( $scores[$qid] ) ) { |
158 | | - $wgOut->addWikiMsg( 'securepoll-tally-error' ); |
159 | | - return; |
160 | | - } |
161 | | - if ( !$talliers[$qid]->addVote( $scores[$qid] ) ) { |
162 | | - $wgOut->addWikiMsg( 'securepoll-tally-error' ); |
163 | | - return; |
164 | | - } |
165 | | - } |
| 128 | + $status = $this->election->tally(); |
| 129 | + if ( !$status->isOK() ) { |
| 130 | + $wgOut->addWikiText( $status->getWikiText() ); |
| 131 | + return; |
166 | 132 | } |
167 | | - $first = true; |
168 | | - foreach ( $questions as $question ) { |
169 | | - if ( $first ) { |
170 | | - $first = false; |
171 | | - } else { |
172 | | - $wgOut->addHTML( "<hr/>\n" ); |
173 | | - } |
174 | | - $tallier = $talliers[$question->getId()]; |
175 | | - $tallier->finishTally(); |
176 | | - $wgOut->addHTML( |
177 | | - $question->parseMessage( 'text' ) . |
178 | | - $tallier->getHtmlResult() ); |
179 | | - } |
| 133 | + $tallier = $status->value; |
| 134 | + $wgOut->addHTML( $tallier->getHtmlResult() ); |
180 | 135 | } |
181 | 136 | |
182 | 137 | /** |
— | — | @@ -183,8 +138,6 @@ |
184 | 139 | */ |
185 | 140 | function submitUpload() { |
186 | 141 | global $wgOut; |
187 | | - $crypt = $this->election->getCrypt(); |
188 | | - $tallier = $this->election->getTallier(); |
189 | 142 | if ( !isset( $_FILES['tally_file'] ) |
190 | 143 | || !is_uploaded_file( $_FILES['tally_file']['tmp_name'] ) |
191 | 144 | || !$_FILES['tally_file']['size'] ) |
— | — | @@ -192,24 +145,20 @@ |
193 | 146 | $wgOut->addWikiMsg( 'securepoll-no-upload' ); |
194 | 147 | return; |
195 | 148 | } |
196 | | - |
197 | | - $fileString = file_get_contents( $_FILES['tally_file']['tmp_name'] ); |
198 | | - $records = StringUtils::explode( "\n\n\n", $fileString ); |
199 | | - foreach ( $records as $encrypted ) { |
200 | | - if ( trim( $encrypted ) == '' ) { |
201 | | - continue; |
202 | | - } |
203 | | - $status = $crypt->decrypt( $encrypted ); |
204 | | - if ( !$status->isOK() ) { |
205 | | - $wgOut->addWikiText( $status->getWikiText() ); |
206 | | - return; |
207 | | - } |
208 | | - if ( !$tallier->addRecord( $status->value ) ) { |
209 | | - $wgOut->addWikiMsg( 'securepoll-tally-error' ); |
210 | | - return; |
211 | | - } |
| 149 | + $context = SecurePoll_Context::newFromXmlFile( $_FILES['tally_file']['tmp_name'] ); |
| 150 | + if ( !$context ) { |
| 151 | + $wgOut->addWikiMsg( 'securepoll-dump-corrupt' ); |
| 152 | + return; |
212 | 153 | } |
213 | | - $tallier->finishTally(); |
| 154 | + $electionIds = $context->getStore()->getAllElectionIds(); |
| 155 | + $election = $context->getElection( reset( $electionIds ) ); |
| 156 | + |
| 157 | + $status = $election->tally(); |
| 158 | + if ( !$status->isOK() ) { |
| 159 | + $wgOut->addWikiText( $status->getWikiText( 'securepoll-tally-upload-error' ) ); |
| 160 | + return; |
| 161 | + } |
| 162 | + $tallier = $status->value; |
214 | 163 | $wgOut->addHTML( $tallier->getHtmlResult() ); |
215 | 164 | } |
216 | 165 | |
Index: trunk/extensions/SecurePoll/includes/DetailsPage.php |
— | — | @@ -17,7 +17,8 @@ |
18 | 18 | } |
19 | 19 | |
20 | 20 | $this->voteId = intval( $params[0] ); |
21 | | - $db = wfGetDB( DB_MASTER ); |
| 21 | + |
| 22 | + $db = $this->context->getDB(); |
22 | 23 | $row = $db->selectRow( |
23 | 24 | array( 'securepoll_votes', 'securepoll_elections', 'securepoll_voters' ), |
24 | 25 | '*', |
— | — | @@ -33,8 +34,13 @@ |
34 | 35 | return; |
35 | 36 | } |
36 | 37 | |
37 | | - $this->election = SecurePoll_Election::newFromRow( $row ); |
| 38 | + $this->election = $this->context->newElectionFromRow( $row ); |
38 | 39 | $this->initLanguage( $wgUser, $this->election ); |
| 40 | + |
| 41 | + $this->parent->setSubtitle( array( |
| 42 | + $this->parent->getTitle( 'list/' . $this->election->getId() ), |
| 43 | + wfMsg( 'securepoll-list-title', $this->election->getMessage( 'title' ) ) ) ); |
| 44 | + |
39 | 45 | if ( !$this->election->isAdmin( $wgUser ) ) { |
40 | 46 | $wgOut->addWikiMsg( 'securepoll-need-admin' ); |
41 | 47 | return; |
Index: trunk/extensions/SecurePoll/includes/Context.php |
— | — | @@ -0,0 +1,279 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * This object contains caches and various items of processing context for |
| 6 | + * SecurePoll. It manages instances of long-lived objects such as the |
| 7 | + * SecurePoll_Store subclass. |
| 8 | + * |
| 9 | + * Long-lived data should be stored here, rather than in global variables or |
| 10 | + * static member variables. |
| 11 | + * |
| 12 | + * A context object is passed to almost all SecurePoll constructors. This class |
| 13 | + * provides factory functions for these objects, to simplify object creation |
| 14 | + * and avoid having to use the SecurePoll_* prefixed class names. |
| 15 | + * |
| 16 | + * For debugging purposes, a var_dump() workalike which omits context objects |
| 17 | + * is available as $context->varDump(). |
| 18 | + */ |
| 19 | +class SecurePoll_Context { |
| 20 | + /** Language fallback sequence */ |
| 21 | + public $languages = array( 'en' ); |
| 22 | + |
| 23 | + /** Message text cache */ |
| 24 | + var $messageCache = array(); |
| 25 | + |
| 26 | + /** |
| 27 | + * Which messages are loaded. 2-d array: language and entity ID, value arbitrary. |
| 28 | + */ |
| 29 | + var $messagesLoaded = array(); |
| 30 | + |
| 31 | + /** ParserOptions instance used for message parsing */ |
| 32 | + var $parserOptions; |
| 33 | + |
| 34 | + /** The store class, for lazy loading */ |
| 35 | + var $storeClass = 'SecurePoll_DBStore'; |
| 36 | + |
| 37 | + /** The store object */ |
| 38 | + var $store; |
| 39 | + |
| 40 | + /** The SecurePoll_Random instance */ |
| 41 | + var $random; |
| 42 | + |
| 43 | + /** The Database instance */ |
| 44 | + var $db; |
| 45 | + |
| 46 | + /** |
| 47 | + * Create a new SecurePoll_Context with an XML file as the storage backend. |
| 48 | + * Returns false if there was a problem with the file, like a parse error. |
| 49 | + */ |
| 50 | + static function newFromXmlFile( $fileName ) { |
| 51 | + $context = new self; |
| 52 | + $store = new SecurePoll_XMLStore( $fileName ); |
| 53 | + $context->setStore( $store ); |
| 54 | + $success = $store->readFile(); |
| 55 | + if ( $success ) { |
| 56 | + return $context; |
| 57 | + } else { |
| 58 | + return false; |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + /** Get the ParserOptions instance */ |
| 63 | + function getParserOptions() { |
| 64 | + if ( !$this->parserOptions ) { |
| 65 | + $this->parserOptions = new ParserOptions; |
| 66 | + } |
| 67 | + return $this->parserOptions; |
| 68 | + } |
| 69 | + |
| 70 | + /** Get the SecurePoll_Store instance */ |
| 71 | + function getStore() { |
| 72 | + if ( !isset( $this->store ) ) { |
| 73 | + $this->store = new $this->storeClass; |
| 74 | + } |
| 75 | + return $this->store; |
| 76 | + } |
| 77 | + |
| 78 | + /** Set the store class */ |
| 79 | + function setStoreClass( $class ) { |
| 80 | + $this->store = null; |
| 81 | + $this->storeClass = $class; |
| 82 | + } |
| 83 | + |
| 84 | + /** Set the store object. Overrides any previous store class. */ |
| 85 | + function setStore( $store ) { |
| 86 | + $this->store = $store; |
| 87 | + } |
| 88 | + |
| 89 | + /** |
| 90 | + * Get an election object from the store, with a given entity ID. Returns |
| 91 | + * false if it does not exist. |
| 92 | + */ |
| 93 | + function getElection( $id ) { |
| 94 | + $info = $this->getStore()->getElectionInfo( array( $id ) ); |
| 95 | + if ( $info ) { |
| 96 | + return $this->newElection( reset( $info ) ); |
| 97 | + } else { |
| 98 | + return false; |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * Get an election object from the store, with a given name. Returns false |
| 104 | + * if there is no such election. |
| 105 | + */ |
| 106 | + function getElectionByTitle( $name ) { |
| 107 | + $info = $this->getStore()->getElectionInfoByTitle( array( $name ) ); |
| 108 | + if ( $info ) { |
| 109 | + return $this->newElection( reset( $info ) ); |
| 110 | + } else { |
| 111 | + return false; |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + /** |
| 116 | + * Get an election object from a securepoll_elections DB row. This will fail |
| 117 | + * if the current store class does not support database operations. |
| 118 | + */ |
| 119 | + function newElectionFromRow( $row ) { |
| 120 | + $info = $this->getStore()->decodeElectionRow( $row ); |
| 121 | + return $this->newElection( $info ); |
| 122 | + } |
| 123 | + |
| 124 | + /** |
| 125 | + * Get a SecurePoll_Random instance. This provides cryptographic random |
| 126 | + * number generation. |
| 127 | + */ |
| 128 | + function getRandom() { |
| 129 | + if ( !$this->random ) { |
| 130 | + $this->random = new SecurePoll_Random; |
| 131 | + } |
| 132 | + return $this->random; |
| 133 | + } |
| 134 | + /** |
| 135 | + * Set the global language fallback sequence. |
| 136 | + * |
| 137 | + * @param $languages array A list of language codes. When a message is |
| 138 | + * requested, the first code in the array will be tried first, followed |
| 139 | + * by the subsequent codes. |
| 140 | + */ |
| 141 | + function setLanguages( $languages ) { |
| 142 | + $this->languages = $languages; |
| 143 | + } |
| 144 | + |
| 145 | + /** |
| 146 | + * Get some messages from the backend store or the cache. |
| 147 | + * This is an internal interface for SecurePoll_Entity, generally you |
| 148 | + * should use SecurePoll_Entity::getMessage() instead. |
| 149 | + * |
| 150 | + * @param $lang Language code |
| 151 | + * @param $ids Entity IDs |
| 152 | + */ |
| 153 | + function getMessages( $lang, $ids ) { |
| 154 | + if ( isset( $this->messagesLoaded[$lang] ) ) { |
| 155 | + $cacheRow = $this->messagesLoaded[$lang]; |
| 156 | + $uncachedIds = array_flip( $ids ); |
| 157 | + foreach ( $uncachedIds as $id => $unused ) { |
| 158 | + if ( isset( $cacheRow[$id] ) ) { |
| 159 | + unset( $uncachedIds[$id] ); |
| 160 | + } |
| 161 | + } |
| 162 | + if ( count( $uncachedIds ) ) { |
| 163 | + $messages = $this->getStore()->getMessages( $lang, array_keys( $uncachedIds ) ); |
| 164 | + $this->messageCache[$lang] = $this->messageCache[$lang] + $messages; |
| 165 | + $this->messagesLoaded[$lang] = $this->messagesLoaded[$lang] + $uncachedIds; |
| 166 | + } |
| 167 | + return array_intersect_key( $this->messageCache[$lang], array_flip( $ids ) ); |
| 168 | + } else { |
| 169 | + $this->messagesLoaded[$lang] = $ids; |
| 170 | + $this->messageCache[$lang] = $this->getStore()->getMessages( $lang, $ids ); |
| 171 | + return $this->messageCache[$lang]; |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + /** |
| 176 | + * Get a particular message. |
| 177 | + * This is an internal interface for SecurePoll_Entity, generally you |
| 178 | + * should use SecurePoll_Entity::getMessage() instead. |
| 179 | + * |
| 180 | + * @param $lang Language code |
| 181 | + * @param $id Entity ID |
| 182 | + * @param $key Message key |
| 183 | + */ |
| 184 | + function getMessage( $lang, $id, $key ) { |
| 185 | + if ( !isset( $this->messagesLoaded[$lang][$id] ) ) { |
| 186 | + $this->getMessages( $lang, array( $id ) ); |
| 187 | + } |
| 188 | + if ( isset( $this->messageCache[$lang][$id][$key] ) ) { |
| 189 | + return $this->messageCache[$lang][$id][$key]; |
| 190 | + } else { |
| 191 | + return false; |
| 192 | + } |
| 193 | + } |
| 194 | + |
| 195 | + /** |
| 196 | + * Get a database object, or throw an exception if the current store object |
| 197 | + * does not support database operations. |
| 198 | + */ |
| 199 | + function getDB() { |
| 200 | + if ( !isset( $this->db ) ) { |
| 201 | + $this->db = $this->getStore()->getDB(); |
| 202 | + } |
| 203 | + return $this->db; |
| 204 | + } |
| 205 | + |
| 206 | + function newElection( $info ) { |
| 207 | + return new SecurePoll_Election( $this, $info ); |
| 208 | + } |
| 209 | + |
| 210 | + function newQuestion( $info ) { |
| 211 | + return new SecurePoll_Question( $this, $info ); |
| 212 | + } |
| 213 | + |
| 214 | + function newOption( $info ) { |
| 215 | + return new SecurePoll_Option( $this, $info ); |
| 216 | + } |
| 217 | + |
| 218 | + function newCrypt( $type, $election ) { |
| 219 | + return SecurePoll_Crypt::factory( $this, $type, $election ); |
| 220 | + } |
| 221 | + |
| 222 | + function newTallier( $type, $question ) { |
| 223 | + return SecurePoll_Tallier::factory( $this, $type, $question ); |
| 224 | + } |
| 225 | + |
| 226 | + function newBallot( $type, $election ) { |
| 227 | + return SecurePoll_Ballot::factory( $this, $type, $election ); |
| 228 | + } |
| 229 | + |
| 230 | + function newAuth( $type ) { |
| 231 | + return SecurePoll_Auth::factory( $this, $type ); |
| 232 | + } |
| 233 | + |
| 234 | + function newElectionTallier( $election ) { |
| 235 | + return new SecurePoll_ElectionTallier( $this, $election ); |
| 236 | + } |
| 237 | + |
| 238 | + /** |
| 239 | + * Debugging function to output a representation of a mixed-type variable, |
| 240 | + * but omitting the $obj->context member variables for brevity. |
| 241 | + * |
| 242 | + * @param $var mixed |
| 243 | + * @param $return True to return the text instead of echoing |
| 244 | + * @param $level Recursion level, leave this as zero when calling. |
| 245 | + */ |
| 246 | + function varDump( $var, $return = false, $level = 0 ) { |
| 247 | + $tab = ' '; |
| 248 | + $indent = str_repeat( $tab, $level ); |
| 249 | + if ( is_array( $var ) ) { |
| 250 | + $s = "array(\n"; |
| 251 | + foreach ( $var as $key => $value ) { |
| 252 | + $s .= "$indent$tab" . $this->varDump( $key, true, $level + 1 ) . " => " . |
| 253 | + $this->varDump( $value, true, $level + 1 ) . ",\n"; |
| 254 | + } |
| 255 | + $s .= "{$indent})"; |
| 256 | + } elseif ( is_object( $var ) ) { |
| 257 | + $props = (array)$var; |
| 258 | + $s = get_class( $var ) . " {\n"; |
| 259 | + foreach ( $props as $key => $value ) { |
| 260 | + $s .= "$indent$tab" . $this->varDump( $key, true, $level + 1 ) . " => "; |
| 261 | + if ( $key === 'context' ) { |
| 262 | + $s .= "[CONTEXT],\n"; |
| 263 | + } else { |
| 264 | + $s .= $this->varDump( $value, true, $level + 1 ) . ",\n"; |
| 265 | + } |
| 266 | + } |
| 267 | + $s .= "{$indent}}"; |
| 268 | + } else { |
| 269 | + $s = var_export( $var, true ); |
| 270 | + } |
| 271 | + if ( $level == 0 ) { |
| 272 | + $s .= "\n"; |
| 273 | + } |
| 274 | + if ( $return ) { |
| 275 | + return $s; |
| 276 | + } else { |
| 277 | + echo $s; |
| 278 | + } |
| 279 | + } |
| 280 | +} |
Property changes on: trunk/extensions/SecurePoll/includes/Context.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 281 | + native |
Index: trunk/extensions/SecurePoll/includes/TranslatePage.php |
— | — | @@ -17,7 +17,7 @@ |
18 | 18 | } |
19 | 19 | |
20 | 20 | $electionId = intval( $params[0] ); |
21 | | - $this->election = SecurePoll::getElection( $electionId ); |
| 21 | + $this->election = $this->context->getElection( $electionId ); |
22 | 22 | if ( !$this->election ) { |
23 | 23 | $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId ); |
24 | 24 | return; |
— | — | @@ -53,6 +53,11 @@ |
54 | 54 | return; |
55 | 55 | } |
56 | 56 | |
| 57 | + # Set a subtitle to return to the language selector |
| 58 | + $this->parent->setSubtitle( array( |
| 59 | + $this->getTitle(), |
| 60 | + wfMsg( 'securepoll-translate-title', $this->election->getMessage( 'title' ) ) ) ); |
| 61 | + |
57 | 62 | # If the request was posted, do the submit |
58 | 63 | if ( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit' ) { |
59 | 64 | $this->doSubmit( $secondary ); |
— | — | @@ -102,7 +107,7 @@ |
103 | 108 | /** |
104 | 109 | * @return Title |
105 | 110 | */ |
106 | | - function getTitle( $lang ) { |
| 111 | + function getTitle( $lang = false ) { |
107 | 112 | $subpage = 'translate/' . $this->election->getId(); |
108 | 113 | if ( $lang !== false ) { |
109 | 114 | $subpage .= '/' . $lang; |
— | — | @@ -166,7 +171,7 @@ |
167 | 172 | } |
168 | 173 | } |
169 | 174 | if ( $replaceBatch ) { |
170 | | - $dbw = wfGetDB( DB_MASTER ); |
| 175 | + $dbw = $this->context->getDB(); |
171 | 176 | $dbw->replace( |
172 | 177 | 'securepoll_msgs', |
173 | 178 | array( array( 'msg_entity', 'msg_lang', 'msg_key' ) ), |
Index: trunk/extensions/SecurePoll/includes/Tallier.php |
— | — | @@ -1,7 +1,7 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | 4 | abstract class SecurePoll_Tallier { |
5 | | - var $question; |
| 5 | + var $context, $question; |
6 | 6 | |
7 | 7 | abstract function addVote( $scores ); |
8 | 8 | abstract function getHtmlResult(); |
— | — | @@ -9,18 +9,19 @@ |
10 | 10 | |
11 | 11 | abstract function finishTally(); |
12 | 12 | |
13 | | - static function factory( $type, $question ) { |
| 13 | + static function factory( $context, $type, $question ) { |
14 | 14 | switch ( $type ) { |
15 | 15 | case 'plurality': |
16 | | - return new SecurePoll_PluralityTallier( $question ); |
| 16 | + return new SecurePoll_PluralityTallier( $context, $question ); |
17 | 17 | case 'schulze': |
18 | | - return new SecurePoll_SchulzeTallier( $question ); |
| 18 | + return new SecurePoll_SchulzeTallier( $context, $question ); |
19 | 19 | default: |
20 | 20 | throw new MWException( "Invalid tallier type: $type" ); |
21 | 21 | } |
22 | 22 | } |
23 | 23 | |
24 | | - function __construct( $question ) { |
| 24 | + function __construct( $context, $question ) { |
| 25 | + $this->context = $context; |
25 | 26 | $this->question = $question; |
26 | 27 | } |
27 | 28 | } |
— | — | @@ -31,8 +32,8 @@ |
32 | 33 | class SecurePoll_PluralityTallier extends SecurePoll_Tallier { |
33 | 34 | var $tally = array(); |
34 | 35 | |
35 | | - function __construct( $question ) { |
36 | | - parent::__construct( $question ); |
| 36 | + function __construct( $context, $question ) { |
| 37 | + parent::__construct( $context, $question ); |
37 | 38 | foreach ( $question->getOptions() as $option ) { |
38 | 39 | $this->tally[$option->getId()] = 0; |
39 | 40 | } |
— | — | @@ -56,7 +57,7 @@ |
57 | 58 | |
58 | 59 | function getHtmlResult() { |
59 | 60 | // Show the results |
60 | | - $s = ''; |
| 61 | + $s = "<table class=\"securepoll-results\">\n"; |
61 | 62 | |
62 | 63 | foreach ( $this->question->getOptions() as $option ) { |
63 | 64 | $s .= '<tr><td>' . $option->getMessage( 'text' ) . "</td>\n" . |
— | — | @@ -70,7 +71,7 @@ |
71 | 72 | function getTextResult() { |
72 | 73 | // Calculate column width |
73 | 74 | $width = 10; |
74 | | - foreach ( $question->getOptions() as $option ) { |
| 75 | + foreach ( $this->question->getOptions() as $option ) { |
75 | 76 | $width = max( $width, strlen( $option->getMessage( 'text' ) ) ); |
76 | 77 | } |
77 | 78 | if ( $width > 57 ) { |
— | — | @@ -78,8 +79,12 @@ |
79 | 80 | } |
80 | 81 | |
81 | 82 | // Show the results |
82 | | - $s = wordwrap( $question->getMessage( 'text' ) ) . "\n"; |
83 | | - foreach ( $question->getOptions() as $option ) { |
| 83 | + $qtext = $this->question->getMessage( 'text' ); |
| 84 | + $s = ''; |
| 85 | + if ( $qtext !== '' ) { |
| 86 | + $s .= wordwrap( $qtext ) . "\n"; |
| 87 | + } |
| 88 | + foreach ( $this->question->getOptions() as $option ) { |
84 | 89 | $otext = $option->getMessage( 'text' ); |
85 | 90 | if ( strlen( $otext ) > $width ) { |
86 | 91 | $otext = substr( $otext, 0, $width - 3 ) . '...'; |
— | — | @@ -87,7 +92,7 @@ |
88 | 93 | $otext = str_pad( $otext, $width ); |
89 | 94 | } |
90 | 95 | $s .= $otext . ' | ' . |
91 | | - $this->tally[$question->getId()][$option->getId()] . "\n"; |
| 96 | + $this->tally[$option->getId()] . "\n"; |
92 | 97 | } |
93 | 98 | return $s; |
94 | 99 | } |
— | — | @@ -111,8 +116,8 @@ |
112 | 117 | var $optionIds = array(); |
113 | 118 | var $victories = array(); |
114 | 119 | |
115 | | - function __construct( $question ) { |
116 | | - parent::__construct( $question ); |
| 120 | + function __construct( $context, $question ) { |
| 121 | + parent::__construct( $context, $question ); |
117 | 122 | $this->optionIds = array(); |
118 | 123 | foreach ( $question->getOptions() as $option ) { |
119 | 124 | $this->optionIds[] = $option->getId(); |
— | — | @@ -218,7 +223,6 @@ |
219 | 224 | |
220 | 225 | function getHtmlResult() { |
221 | 226 | return '<pre>' . $this->getTextResult() . '</pre>'; |
222 | | - |
223 | 227 | } |
224 | 228 | |
225 | 229 | function getTextResult() { |
Index: trunk/extensions/SecurePoll/includes/Store.php |
— | — | @@ -0,0 +1,549 @@ |
| 2 | +<?php |
| 3 | + |
| 4 | +/** |
| 5 | + * This is an abstraction of the persistence layer, to allow XML dumps to be |
| 6 | + * operated on and tallied, like elections in the local DB. |
| 7 | + * |
| 8 | + * Most of the UI layer has no need for this abstraction, and so we provide |
| 9 | + * direct database access via getDB() to ease development of those components. |
| 10 | + * The XML store will throw an exception if getDB() is called on it. |
| 11 | + * |
| 12 | + * Most of the functions here are internal interfaces for the use of |
| 13 | + * the entity classes (election, question and option). The entity classes |
| 14 | + * and SecurePoll_Context provide methods that are more appropriate for general |
| 15 | + * users. |
| 16 | + */ |
| 17 | +interface SecurePoll_Store { |
| 18 | + /** |
| 19 | + * Get an array of messages with a given language, and entity IDs |
| 20 | + * in a given array of IDs. The return format is a 2-d array mapping ID |
| 21 | + * and message key to value. |
| 22 | + */ |
| 23 | + function getMessages( $lang, $ids ); |
| 24 | + |
| 25 | + /** |
| 26 | + * Get an array of properties for a given set of IDs. Returns a 2-d array |
| 27 | + * mapping IDs and property keys to values. |
| 28 | + */ |
| 29 | + function getProperties( $ids ); |
| 30 | + |
| 31 | + /** |
| 32 | + * Get information about a set of elections, specifically the data that |
| 33 | + * is stored in the securepoll_elections row in the DB. Returns a 2-d |
| 34 | + * array mapping ID to associative array of properties. |
| 35 | + */ |
| 36 | + function getElectionInfo( $ids ); |
| 37 | + |
| 38 | + /** |
| 39 | + * Get election information for a given set of names. |
| 40 | + */ |
| 41 | + function getElectionInfoByTitle( $names ); |
| 42 | + |
| 43 | + /** |
| 44 | + * Convert a row from the securepoll_elections table into an associative |
| 45 | + * array suitable for return by getElectionInfo(). |
| 46 | + */ |
| 47 | + function decodeElectionRow( $row ); |
| 48 | + |
| 49 | + /** |
| 50 | + * Get a database connection object. |
| 51 | + */ |
| 52 | + function getDB(); |
| 53 | + |
| 54 | + /** |
| 55 | + * Get an associative array of information about all questions in a given |
| 56 | + * election. |
| 57 | + */ |
| 58 | + function getQuestionInfo( $electionId ); |
| 59 | + |
| 60 | + /** |
| 61 | + * Call a callback function for all valid votes with a given election ID. |
| 62 | + */ |
| 63 | + function callbackValidVotes( $electionId, $callback ); |
| 64 | +} |
| 65 | + |
| 66 | +/** |
| 67 | + * Storage class for a DB backend. This is the one that's most often used. |
| 68 | + */ |
| 69 | +class SecurePoll_DBStore implements SecurePoll_Store { |
| 70 | + function getMessages( $lang, $ids ) { |
| 71 | + $db = $this->getDB(); |
| 72 | + $res = $db->select( |
| 73 | + 'securepoll_msgs', |
| 74 | + '*', |
| 75 | + array( |
| 76 | + 'msg_entity' => $ids, |
| 77 | + 'msg_lang' => $lang |
| 78 | + ), |
| 79 | + __METHOD__ |
| 80 | + ); |
| 81 | + $messages = array(); |
| 82 | + foreach ( $res as $row ) { |
| 83 | + $messages[$row->msg_entity][$row->msg_key] = $row->msg_text; |
| 84 | + } |
| 85 | + return $messages; |
| 86 | + } |
| 87 | + |
| 88 | + function getProperties( $ids ) { |
| 89 | + $db = $this->getDB(); |
| 90 | + $res = $db->select( |
| 91 | + 'securepoll_properties', |
| 92 | + '*', |
| 93 | + array( 'pr_entity' => $ids ), |
| 94 | + __METHOD__ ); |
| 95 | + $properties = array(); |
| 96 | + foreach ( $res as $row ) { |
| 97 | + $properties[$row->pr_entity][$row->pr_key] = $row->pr_value; |
| 98 | + } |
| 99 | + return $properties; |
| 100 | + } |
| 101 | + |
| 102 | + function getElectionInfo( $ids ) { |
| 103 | + $ids = (array)$ids; |
| 104 | + $db = $this->getDB(); |
| 105 | + $res = $db->select( |
| 106 | + 'securepoll_elections', |
| 107 | + '*', |
| 108 | + array( 'el_entity' => $ids ), |
| 109 | + __METHOD__ ); |
| 110 | + $infos = array(); |
| 111 | + foreach ( $res as $row ) { |
| 112 | + $infos[$row->el_entity] = $this->decodeElectionRow( $row ); |
| 113 | + } |
| 114 | + return $infos; |
| 115 | + } |
| 116 | + |
| 117 | + function getElectionInfoByTitle( $names ) { |
| 118 | + $names = (array)$names; |
| 119 | + $db = $this->getDB(); |
| 120 | + $res = $db->select( |
| 121 | + 'securepoll_elections', |
| 122 | + '*', |
| 123 | + array( 'el_title' => $names ), |
| 124 | + __METHOD__ ); |
| 125 | + $infos = array(); |
| 126 | + foreach ( $res as $row ) { |
| 127 | + $infos[$row->el_title] = $this->decodeElectionRow( $row ); |
| 128 | + } |
| 129 | + return $infos; |
| 130 | + } |
| 131 | + |
| 132 | + function decodeElectionRow( $row ) { |
| 133 | + static $map = array( |
| 134 | + 'id' => 'el_entity', |
| 135 | + 'title' => 'el_title', |
| 136 | + 'ballot' => 'el_ballot', |
| 137 | + 'tally' => 'el_tally', |
| 138 | + 'primaryLang' => 'el_primary_lang', |
| 139 | + 'startDate' => 'el_start_date', |
| 140 | + 'endDate' => 'el_end_date', |
| 141 | + 'auth' => 'el_auth_type' |
| 142 | + ); |
| 143 | + |
| 144 | + $info = array(); |
| 145 | + foreach ( $map as $key => $field ) { |
| 146 | + if ( $key == 'startDate' || $key == 'endDate' ) { |
| 147 | + $info[$key] = wfTimestamp( TS_MW, $row->$field ); |
| 148 | + } else { |
| 149 | + $info[$key] = $row->$field; |
| 150 | + } |
| 151 | + } |
| 152 | + return $info; |
| 153 | + } |
| 154 | + |
| 155 | + function getDB() { |
| 156 | + return wfGetDB( DB_MASTER ); |
| 157 | + } |
| 158 | + |
| 159 | + function getQuestionInfo( $electionId ) { |
| 160 | + $db = $this->getDB(); |
| 161 | + $res = $db->select( |
| 162 | + array( 'securepoll_questions', 'securepoll_options' ), |
| 163 | + '*', |
| 164 | + array( |
| 165 | + 'qu_election' => $electionId, |
| 166 | + 'op_question=qu_entity' |
| 167 | + ), |
| 168 | + __METHOD__, |
| 169 | + array( 'ORDER BY' => 'qu_index, qu_entity' ) |
| 170 | + ); |
| 171 | + |
| 172 | + $questions = array(); |
| 173 | + $options = array(); |
| 174 | + $questionId = false; |
| 175 | + foreach ( $res as $row ) { |
| 176 | + if ( $questionId === false ) { |
| 177 | + } elseif ( $questionId !== $row->qu_entity ) { |
| 178 | + $questions[] = array( 'id' => $questionId, 'options' => $options ); |
| 179 | + $options = array(); |
| 180 | + } |
| 181 | + $options[] = array( 'id' => $row->op_entity ); |
| 182 | + $questionId = $row->qu_entity; |
| 183 | + } |
| 184 | + if ( $questionId !== false ) { |
| 185 | + $questions[] = array( 'id' => $questionId, 'options' => $options ); |
| 186 | + } |
| 187 | + return $questions; |
| 188 | + } |
| 189 | + |
| 190 | + function callbackValidVotes( $electionId, $callback ) { |
| 191 | + $dbr = $this->getDB(); |
| 192 | + $res = $dbr->select( |
| 193 | + 'securepoll_votes', |
| 194 | + array( 'vote_record' ), |
| 195 | + array( |
| 196 | + 'vote_election' => $electionId, |
| 197 | + 'vote_current' => 1, |
| 198 | + 'vote_struck' => 0 |
| 199 | + ), __METHOD__ |
| 200 | + ); |
| 201 | + foreach ( $res as $row ) { |
| 202 | + $status = call_user_func( $callback, $this, $row->vote_record ); |
| 203 | + if ( $status && !$status->isOK() ) { |
| 204 | + return $status; |
| 205 | + } |
| 206 | + } |
| 207 | + return Status::newGood(); |
| 208 | + } |
| 209 | +} |
| 210 | + |
| 211 | +/** |
| 212 | + * Storage class that stores all data in local memory. The memory must be |
| 213 | + * initialised somehow, methods for this are not provided except in the |
| 214 | + * subclass. |
| 215 | + */ |
| 216 | +class SecurePoll_MemoryStore implements SecurePoll_Store { |
| 217 | + var $messages, $properties, $idsByName, $votes; |
| 218 | + var $entityInfo; |
| 219 | + |
| 220 | + /** |
| 221 | + * Get an array containing all election IDs stored in this object |
| 222 | + */ |
| 223 | + function getAllElectionIds() { |
| 224 | + $electionIds = array(); |
| 225 | + foreach ( $this->entityInfo as $info ) { |
| 226 | + if ( $info['type'] !== 'election' ) { |
| 227 | + continue; |
| 228 | + } |
| 229 | + $electionIds[] = $info['id']; |
| 230 | + } |
| 231 | + return $electionIds; |
| 232 | + } |
| 233 | + |
| 234 | + function getMessages( $lang, $ids ) { |
| 235 | + if ( !isset( $this->messages[$lang] ) ) { |
| 236 | + return array(); |
| 237 | + } |
| 238 | + return array_intersect_key( $this->messages[$lang], array_flip( $ids ) ); |
| 239 | + } |
| 240 | + |
| 241 | + function getProperties( $ids ) { |
| 242 | + $ids = (array)$ids; |
| 243 | + return array_intersect_key( $this->properties, array_flip( $ids ) ); |
| 244 | + } |
| 245 | + |
| 246 | + function getElectionInfo( $ids ) { |
| 247 | + $ids = (array)$ids; |
| 248 | + return array_intersect_key( $this->entityInfo, array_flip( $ids ) ); |
| 249 | + } |
| 250 | + |
| 251 | + function getElectionInfoByTitle( $names ) { |
| 252 | + $names = (array)$names; |
| 253 | + $ids = array_intersect_key( $this->idsByName, array_flip( $names ) ); |
| 254 | + $info = array_intersect_key( $this->entityInfo, array_flip( $ids ) ); |
| 255 | + return $info; |
| 256 | + } |
| 257 | + |
| 258 | + function getQuestionInfo( $electionId ) { |
| 259 | + return $this->entityInfo[$electionId]['questions']; |
| 260 | + } |
| 261 | + |
| 262 | + function decodeElectionRow( $row ) { |
| 263 | + throw new MWException( 'Internal error: attempt to use decodeElectionRow() with ' . |
| 264 | + 'a storage class that doesn\'t support it.' ); |
| 265 | + } |
| 266 | + |
| 267 | + function getDB() { |
| 268 | + throw new MWException( 'Internal error: attempt to use getDB() when the database ' . |
| 269 | + 'is disabled.' ); |
| 270 | + } |
| 271 | + |
| 272 | + function callbackValidVotes( $electionId, $callback ) { |
| 273 | + if ( !isset( $this->votes[$electionId] ) ) { |
| 274 | + return Status::newGood(); |
| 275 | + } |
| 276 | + foreach ( $this->votes[$electionId] as $vote ) { |
| 277 | + $status = call_user_func( $callback, $this, $vote ); |
| 278 | + if ( !$status->isOK() ) { |
| 279 | + return $status; |
| 280 | + } |
| 281 | + } |
| 282 | + return Status::newGood(); |
| 283 | + } |
| 284 | +} |
| 285 | + |
| 286 | +/** |
| 287 | + * Storage class for an XML file store. Election configuration data is cached, |
| 288 | + * and vote data can be loaded into a tallier on demand. |
| 289 | + */ |
| 290 | +class SecurePoll_XMLStore extends SecurePoll_MemoryStore { |
| 291 | + var $xmlReader, $fileName; |
| 292 | + var $voteCallback, $voteElectionId; |
| 293 | + |
| 294 | + /** Valid entity info keys by entity type. */ |
| 295 | + static $entityInfoKeys = array( |
| 296 | + 'election' => array( |
| 297 | + 'id', |
| 298 | + 'title', |
| 299 | + 'ballot', |
| 300 | + 'tally', |
| 301 | + 'primaryLang', |
| 302 | + 'startDate', |
| 303 | + 'endDate', |
| 304 | + 'auth' |
| 305 | + ), |
| 306 | + 'question' => array( 'id' ), |
| 307 | + 'option' => array( 'id' ), |
| 308 | + ); |
| 309 | + |
| 310 | + /** The type of each entity child and its corresponding (plural) info element */ |
| 311 | + static $childTypes = array( |
| 312 | + 'election' => array( 'question' => 'questions' ), |
| 313 | + 'question' => array( 'option' => 'options' ), |
| 314 | + 'option' => array() |
| 315 | + ); |
| 316 | + |
| 317 | + /** All entity types */ |
| 318 | + static $entityTypes = array( 'election', 'question', 'option' ); |
| 319 | + |
| 320 | + /** |
| 321 | + * Constructor. Note that readFile() must be called before any information |
| 322 | + * can be accessed. SecurePoll_Context::newFromXmlFile() is a shortcut |
| 323 | + * method for this. |
| 324 | + */ |
| 325 | + function __construct( $fileName ) { |
| 326 | + $this->fileName = $fileName; |
| 327 | + } |
| 328 | + |
| 329 | + /** |
| 330 | + * Read the file and return boolean success. |
| 331 | + */ |
| 332 | + function readFile() { |
| 333 | + $this->xmlReader = new XMLReader; |
| 334 | + $xr = $this->xmlReader; |
| 335 | + $fileName = realpath( $this->fileName ); |
| 336 | + $uri = 'file://' . str_replace( '%2F', '/', rawurlencode( $fileName ) ); |
| 337 | + $xr->open( $uri ); |
| 338 | + $xr->setParserProperty( XMLReader::SUBST_ENTITIES, true ); |
| 339 | + $success = $this->doTopLevel(); |
| 340 | + $xr->close(); |
| 341 | + $this->xmlReader = null; |
| 342 | + return $success; |
| 343 | + } |
| 344 | + |
| 345 | + /** |
| 346 | + * Do the top-level document element, and return success. |
| 347 | + */ |
| 348 | + function doTopLevel() { |
| 349 | + $xr = $this->xmlReader; |
| 350 | + |
| 351 | + # Check document element |
| 352 | + while ( $xr->read() && $xr->nodeType !== XMLReader::ELEMENT ); |
| 353 | + if ( $xr->name != 'SecurePoll' ) { |
| 354 | + wfDebug( __METHOD__.": invalid document element\n" ); |
| 355 | + return false; |
| 356 | + } |
| 357 | + |
| 358 | + while ( $xr->read() ) { |
| 359 | + if ( $xr->nodeType !== XMLReader::ELEMENT ) { |
| 360 | + continue; |
| 361 | + } |
| 362 | + if ( $xr->name !== 'election' ) { |
| 363 | + continue; |
| 364 | + } |
| 365 | + if ( !$this->doElection() ) { |
| 366 | + return false; |
| 367 | + } |
| 368 | + } |
| 369 | + return true; |
| 370 | + } |
| 371 | + |
| 372 | + /** |
| 373 | + * Read an <election> element and position the cursor past the end of it. |
| 374 | + * Return success. |
| 375 | + */ |
| 376 | + function doElection() { |
| 377 | + $xr = $this->xmlReader; |
| 378 | + if ( $xr->isEmptyElement ) { |
| 379 | + wfDebug( __METHOD__.": unexpected empty element\n" ); |
| 380 | + return false; |
| 381 | + } |
| 382 | + $xr->read(); |
| 383 | + $electionInfo = false; |
| 384 | + while ( $xr->nodeType !== XMLReader::NONE ) { |
| 385 | + if ( $xr->nodeType === XMLReader::END_ELEMENT ) { |
| 386 | + # Finished |
| 387 | + return true; |
| 388 | + } |
| 389 | + if ( $xr->nodeType !== XMLReader::ELEMENT ) { |
| 390 | + # Skip comments, intervening text, etc. |
| 391 | + $xr->read(); |
| 392 | + continue; |
| 393 | + } |
| 394 | + if ( $xr->name === 'configuration' ) { |
| 395 | + # Load configuration |
| 396 | + $electionInfo = $this->readEntity( 'election' ); |
| 397 | + if ( $electionInfo === false ) { |
| 398 | + return false; |
| 399 | + } |
| 400 | + continue; |
| 401 | + } |
| 402 | + |
| 403 | + if ( $xr->name === 'vote' ) { |
| 404 | + # Notify tallier of vote record if requested |
| 405 | + if ( $this->voteCallback && $electionInfo |
| 406 | + && $electionInfo['id'] == $this->voteElectionId ) |
| 407 | + { |
| 408 | + $record = $this->readStringElement(); |
| 409 | + call_user_func( $this->voteCallback, $this, $record ); |
| 410 | + } else { |
| 411 | + $xr->next(); |
| 412 | + } |
| 413 | + continue; |
| 414 | + } |
| 415 | + |
| 416 | + wfDebug( __METHOD__.": ignoring unrecognised element <{$xr->name}>\n" ); |
| 417 | + $xr->next(); |
| 418 | + } |
| 419 | + wfDebug( __METHOD__.": unexpected end of stream\n" ); |
| 420 | + return false; |
| 421 | + } |
| 422 | + |
| 423 | + /** |
| 424 | + * Read an entity configuration element: <configuration>, <question> or |
| 425 | + * <option>, and position the cursor past the end of it. |
| 426 | + * |
| 427 | + * This function operates recursively to read child elements. It returns |
| 428 | + * the info array for the entity. |
| 429 | + */ |
| 430 | + function readEntity( $entityType ) { |
| 431 | + $xr = $this->xmlReader; |
| 432 | + $info = array( 'type' => $entityType ); |
| 433 | + $messages = array(); |
| 434 | + $properties = array(); |
| 435 | + $children = array(); |
| 436 | + if ( $xr->isEmptyElement ) { |
| 437 | + wfDebug( __METHOD__.": unexpected empty element\n" ); |
| 438 | + $xr->read(); |
| 439 | + return false; |
| 440 | + } |
| 441 | + $xr->read(); |
| 442 | + |
| 443 | + while ( true ) { |
| 444 | + if ( $xr->nodeType === XMLReader::NONE ) { |
| 445 | + wfDebug( __METHOD__.": unexpected end of stream\n" ); |
| 446 | + return false; |
| 447 | + } |
| 448 | + if ( $xr->nodeType === XMLReader::END_ELEMENT ) { |
| 449 | + # End of entity |
| 450 | + $xr->read(); |
| 451 | + break; |
| 452 | + } |
| 453 | + if ( $xr->nodeType !== XMLReader::ELEMENT ) { |
| 454 | + # Intervening text, comments, etc. |
| 455 | + $xr->read(); |
| 456 | + continue; |
| 457 | + } |
| 458 | + if ( $xr->name === 'message' ) { |
| 459 | + $name = $xr->getAttribute( 'name' ); |
| 460 | + $lang = $xr->getAttribute( 'lang' ); |
| 461 | + $value = $this->readStringElement(); |
| 462 | + $messages[$lang][$name] = $value; |
| 463 | + continue; |
| 464 | + } |
| 465 | + if ( $xr->name == 'property' ) { |
| 466 | + $name = $xr->getAttribute( 'name' ); |
| 467 | + $value = $this->readStringElement(); |
| 468 | + $properties[$name] = $value; |
| 469 | + continue; |
| 470 | + } |
| 471 | + |
| 472 | + # Info elements |
| 473 | + if ( in_array( $xr->name, self::$entityInfoKeys[$entityType] ) ) { |
| 474 | + $info[$xr->name] = $this->readStringElement(); |
| 475 | + continue; |
| 476 | + } |
| 477 | + |
| 478 | + # Child elements |
| 479 | + if ( isset( self::$childTypes[$entityType][$xr->name] ) ) { |
| 480 | + $infoKey = self::$childTypes[$entityType][$xr->name]; |
| 481 | + $childInfo = $this->readEntity( $xr->name ); |
| 482 | + if ( !$childInfo ) { |
| 483 | + return false; |
| 484 | + } |
| 485 | + $info[$infoKey][] = $childInfo; |
| 486 | + continue; |
| 487 | + } |
| 488 | + |
| 489 | + wfDebug( __METHOD__.": ignoring unrecognised element <{$xr->name}>\n" ); |
| 490 | + $xr->next(); |
| 491 | + } |
| 492 | + |
| 493 | + if ( !isset( $info['id'] ) ) { |
| 494 | + wfDebug( __METHOD__.": missing id element in <$entityType>\n" ); |
| 495 | + return false; |
| 496 | + } |
| 497 | + $id = $info['id']; |
| 498 | + if ( isset( $info['title'] ) ) { |
| 499 | + $this->idsByName[$info['title']] = $id; |
| 500 | + } |
| 501 | + $this->entityInfo[$id] = $info; |
| 502 | + foreach ( $messages as $lang => $values ) { |
| 503 | + $this->messages[$lang][$id] = $values; |
| 504 | + } |
| 505 | + $this->properties[$id] = $properties; |
| 506 | + return $info; |
| 507 | + } |
| 508 | + |
| 509 | + /** |
| 510 | + * When the cursor is positioned on an element node, this reads the entire |
| 511 | + * element and returns the contents as a string. On return, the cursor is |
| 512 | + * positioned past the end of the element. |
| 513 | + */ |
| 514 | + function readStringElement() { |
| 515 | + $xr = $this->xmlReader; |
| 516 | + if ( $xr->isEmptyElement ) { |
| 517 | + $xr->read(); |
| 518 | + return ''; |
| 519 | + } |
| 520 | + $s = ''; |
| 521 | + $level = 1; |
| 522 | + while ( $xr->read() && $level ) { |
| 523 | + if ( $xr->nodeType == XMLReader::TEXT ) { |
| 524 | + $s .= $xr->value; |
| 525 | + continue; |
| 526 | + } |
| 527 | + if ( $xr->nodeType == XMLReader::ELEMENT && !$xr->isEmptyElement ) { |
| 528 | + $level++; |
| 529 | + continue; |
| 530 | + } |
| 531 | + if ( $xr->nodeType == XMLReader::END_ELEMENT ) { |
| 532 | + $level--; |
| 533 | + continue; |
| 534 | + } |
| 535 | + } |
| 536 | + return $s; |
| 537 | + } |
| 538 | + |
| 539 | + function callbackValidVotes( $electionId, $callback ) { |
| 540 | + $this->voteCallback = $callback; |
| 541 | + $this->voteElectionId = $electionId; |
| 542 | + $success = $this->readFile(); |
| 543 | + $this->voteCallback = $this->voteElectionId = null; |
| 544 | + if ( $success ) { |
| 545 | + return Status::newGood(); |
| 546 | + } else { |
| 547 | + return Status::newFatal( 'securepoll-dump-file-corrupt' ); |
| 548 | + } |
| 549 | + } |
| 550 | +} |
Property changes on: trunk/extensions/SecurePoll/includes/Store.php |
___________________________________________________________________ |
Added: svn:eol-style |
1 | 551 | + native |
Index: trunk/extensions/SecurePoll/cli/dump.php |
— | — | @@ -12,7 +12,8 @@ |
13 | 13 | spFatal( "Usage: php dump.php [-o <outfile>] <election name>" ); |
14 | 14 | } |
15 | 15 | |
16 | | -$election = SecurePoll::getElectionByTitle( $args[0] ); |
| 16 | +$context = new SecurePoll_Context; |
| 17 | +$election = $context->getElectionByTitle( $args[0] ); |
17 | 18 | if ( !$election ) { |
18 | 19 | spFatal( "There is no election called \"$args[0]\"" ); |
19 | 20 | } |
— | — | @@ -23,7 +24,7 @@ |
24 | 25 | $fileName = $options['o']; |
25 | 26 | } |
26 | 27 | if ( $fileName === '-' ) { |
27 | | - $outFile = STDIN; |
| 28 | + $outFile = STDOUT; |
28 | 29 | } else { |
29 | 30 | $outFile = fopen( $fileName, 'w' ); |
30 | 31 | } |
— | — | @@ -31,7 +32,7 @@ |
32 | 33 | spFatal( "Unable to open $fileName for writing" ); |
33 | 34 | } |
34 | 35 | |
35 | | -SecurePoll_Entity::setLanguages( array( $election->getLanguage() ) ); |
| 36 | +$context->setLanguages( array( $election->getLanguage() ) ); |
36 | 37 | |
37 | 38 | $cbdata = array( |
38 | 39 | 'header' => "<SecurePoll>\n<election>\n" . $election->getConfXml(), |
— | — | @@ -45,7 +46,7 @@ |
46 | 47 | spFatal( $status->getWikiText() ); |
47 | 48 | } |
48 | 49 | if ( $election->cbdata['header'] ) { |
49 | | - echo $election->cbdata['header']; |
| 50 | + fwrite( $outFile, $election->cbdata['header'] ); |
50 | 51 | } |
51 | 52 | |
52 | 53 | fwrite( $outFile, "</election>\n</SecurePoll>\n" ); |
— | — | @@ -57,7 +58,7 @@ |
58 | 59 | |
59 | 60 | function spDumpVote( $election, $row ) { |
60 | 61 | if ( $election->cbdata['header'] ) { |
61 | | - echo $election->cbdata['header']; |
| 62 | + fwrite( $election->cbdata['outFile'], $election->cbdata['header'] ); |
62 | 63 | $election->cbdata['header'] = false; |
63 | 64 | } |
64 | 65 | fwrite( $election->cbdata['outFile'], "<vote>" . $row->vote_record . "</vote>\n" ); |
Index: trunk/extensions/SecurePoll/cli/tally.php |
— | — | @@ -5,10 +5,6 @@ |
6 | 6 | * |
7 | 7 | * Can be used to tally very large numbers of votes, when the web interface is |
8 | 8 | * not feasible. |
9 | | - * |
10 | | - * TODO: The entity classes need a bit of refactoring so that they can operate on |
11 | | - * a dump file without having to import it into the database. This will avoid |
12 | | - * some nasty ID collision issues with the import approach. |
13 | 9 | */ |
14 | 10 | |
15 | 11 | $optionsWithArgs = array( 'name' ); |
— | — | @@ -18,90 +14,49 @@ |
19 | 15 | Usage: |
20 | 16 | php tally.php [--html] --name <election name> |
21 | 17 | php tally.php [--html] <dump file> |
22 | | - |
23 | 18 | EOT; |
24 | 19 | |
25 | | -if ( !isset( $options['name'] ) && isset( $args[0] ) ) { |
26 | | - echo "Dump files are not supported yet.\n"; |
27 | | - exit( 1 ); |
28 | | -} elseif( !isset( $options['name'] ) ) { |
29 | | - echo $usage; |
30 | | - exit( 1 ); |
| 20 | +if ( !isset( $options['name'] ) && !isset( $args[0] ) ) { |
| 21 | + spFatal( $usage ); |
31 | 22 | } |
32 | 23 | |
33 | | -if ( !class_exists( 'SecurePoll' ) ) { |
34 | | - # Uninstalled mode |
35 | | - # This may actually work some day, for now it will just give you DB errors |
| 24 | +if ( !class_exists( 'SecurePoll_Context' ) ) { |
| 25 | + if ( isset( $options['name'] ) ) { |
| 26 | + spFatal( "Cannot load from database when SecurePoll is not installed" ); |
| 27 | + } |
36 | 28 | require( dirname( __FILE__ ) . '/../SecurePoll.php' ); |
37 | 29 | } |
38 | | -$election = SecurePoll::getElectionByTitle( $options['name'] ); |
39 | | -if ( !$election ) { |
40 | | - echo "The specified election does not exist.\n"; |
41 | | - exit( 1 ); |
42 | | -} |
43 | | -$election = SecurePoll::getElection( $eid ); |
44 | | -spTallyLocal( $election ); |
45 | 30 | |
46 | | -function spTallyLocal( $election ) { |
47 | | - $dbr = wfGetDB( DB_SLAVE ); |
48 | | - $startId = 0; |
49 | | - $crypt = $election->getCrypt(); |
50 | | - $ballot = $election->getBallot(); |
51 | | - $questions = $election->getQuestions(); |
52 | | - $talliers = $election->getTalliers(); |
53 | | - |
54 | | - while ( true ) { |
55 | | - $res = $dbr->select( |
56 | | - 'securepoll_votes', |
57 | | - array( 'vote_id', 'vote_record' ), |
58 | | - array( |
59 | | - 'vote_election' => $election->getId(), |
60 | | - 'vote_current' => 1, |
61 | | - 'vote_struck' => 0, |
62 | | - 'vote_id > ' . $dbr->addQuotes( $startId ) |
63 | | - ), __METHOD__, |
64 | | - array( 'LIMIT' => 100, 'ORDER BY' => 'vote_id' ) |
65 | | - ); |
66 | | - if ( !$res->numRows() ) { |
67 | | - break; |
68 | | - } |
69 | | - foreach ( $res as $row ) { |
70 | | - var_dump( $row ); |
71 | | - $startId = $row->vote_id; |
72 | | - $record = $row->vote_record; |
73 | | - if ( $crypt ) { |
74 | | - $status = $crypt->decrypt( $record ); |
75 | | - if ( !$status->isOK() ) { |
76 | | - echo $status->getWikiText() . "\n"; |
77 | | - return; |
78 | | - } |
79 | | - $record = $status->value; |
80 | | - } |
81 | | - $record = rtrim( $record ); |
82 | | - $scores = $ballot->unpackRecord( $record ); |
83 | | - foreach ( $questions as $question ) { |
84 | | - $qid = $question->getId(); |
85 | | - if ( !isset( $scores[$qid] ) ) { |
86 | | - echo wfMsg( 'securepoll-tally-error' ) . "\n"; |
87 | | - return; |
88 | | - } |
89 | | - if ( !$talliers[$qid]->addVote( $scores[$qid] ) ) { |
90 | | - echo wfMsg( 'securepoll-tally-error' ) . "\n"; |
91 | | - return; |
92 | | - } |
93 | | - } |
94 | | - } |
| 31 | +$context = new SecurePoll_Context; |
| 32 | +if ( !isset( $options['name'] ) ) { |
| 33 | + $context = SecurePoll_Context::newFromXmlFile( $args[0] ); |
| 34 | + if ( !$context ) { |
| 35 | + spFatal( "Unable to parse XML file \"{$args[0]}\"" ); |
95 | 36 | } |
96 | | - $first = true; |
97 | | - foreach ( $questions as $question ) { |
98 | | - if ( $first ) { |
99 | | - $first = false; |
100 | | - } else { |
101 | | - echo "\n"; |
102 | | - } |
103 | | - $tallier = $talliers[$question->getId()]; |
104 | | - $tallier->finishTally(); |
105 | | - echo $question->getMessage( 'text' ) . "\n" . |
106 | | - $tallier->getTextResult(); |
| 37 | + $electionIds = $context->getStore()->getAllElectionIds(); |
| 38 | + if ( !count( $electionIds ) ) { |
| 39 | + spFatal( "No elections found in XML file \"{$args[0]}\"" ); |
107 | 40 | } |
| 41 | + $election = $context->getElection( reset( $electionIds ) ); |
| 42 | +} else { |
| 43 | + $election = $context->getElectionByTitle( $options['name'] ); |
| 44 | + if ( !$election ) { |
| 45 | + spFatal( "The specified election does not exist." ); |
| 46 | + } |
108 | 47 | } |
| 48 | +$status = $election->tally(); |
| 49 | +if ( !$status->isOK() ) { |
| 50 | + spFatal( "Tally error: " . $status->getWikiText() ); |
| 51 | +} |
| 52 | +$tallier = $status->value; |
| 53 | +if ( isset( $options['html'] ) ) { |
| 54 | + echo $tallier->getHtmlResult(); |
| 55 | +} else { |
| 56 | + echo $tallier->getTextResult(); |
| 57 | +} |
| 58 | + |
| 59 | + |
| 60 | +function spFatal( $message ) { |
| 61 | + fwrite( STDERR, rtrim( $message ) . "\n" ); |
| 62 | + exit( 1 ); |
| 63 | +} |
Index: trunk/extensions/SecurePoll/resources/SecurePoll.css |
— | — | @@ -42,3 +42,24 @@ |
43 | 43 | border-color: red; |
44 | 44 | background-color: #fff2f2; |
45 | 45 | } |
| 46 | +.securepoll-option-preferential { |
| 47 | + margin-bottom: 0.5em; |
| 48 | +} |
| 49 | +.securepoll-results { |
| 50 | + margin: 1em 1em 1em 0; |
| 51 | + background: #f9f9f9; |
| 52 | + border: 1px #aaa solid; |
| 53 | + border-collapse: collapse; |
| 54 | +} |
| 55 | +.securepoll-results th, .securepoll-results td { |
| 56 | + border: 1px #aaa solid; |
| 57 | + padding: 0.4em; |
| 58 | +} |
| 59 | +.securepoll-results th { |
| 60 | + background: #f2f2f2; |
| 61 | + text-align: center; |
| 62 | +} |
| 63 | +.securepoll-results caption { |
| 64 | + font-weight: bold; |
| 65 | +} |
| 66 | + |