r53591 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r53590‎ | r53591 | r53592 >
Date:12:44, 21 July 2009
Author:tstarling
Status:deferred
Tags:
Comment:
Lightly-tested work in progress.

* Implemented zero-configuration file tally feature, as promised. Resolves bug 18972 and bug 18974.
* Introduced "context" and "store" abstractions to allow the backend modules to work with XML files. Lots of refactoring to support this. Replaces the "SecurePoll" static function collection.
* Factored out most of the remaining tally code from the UI components into new class SecurePoll_ElectionTallier.
* Fixed excessive type-hinting in isAdmin(), broke StubUser
* Added subpage-like subtitles, for easier navigation around the UI
* Implemented zero-configuration file tally feature, as promised.
* Introduced "context" and "store" abstractions to allow the backend modules to work with XML files. Lots of refactoring to support this.
* Factored out most of the remaining tally code from the UI components into new class SecurePoll_ElectionTallier.
* Fixed excessive type-hinting in isAdmin(), broke StubUser
* Added subpage-like subtitles, for easier navigation around the UI
* Made SecurePoll_LocalAuth::getUserParams() non-static, to support Context.
Modified paths:
  • /trunk/extensions/SecurePoll/SecurePoll.i18n.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.php (modified) (history)
  • /trunk/extensions/SecurePoll/auth-api.php (modified) (history)
  • /trunk/extensions/SecurePoll/cli/dump.php (modified) (history)
  • /trunk/extensions/SecurePoll/cli/tally.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Auth.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Ballot.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Base.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Context.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/Crypt.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/DetailsPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/DumpPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Election.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ElectionTallier.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/Entity.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/ListPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/LoginPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/MessageDumpPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Option.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Page.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Question.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Store.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/Tallier.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/TallyPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/TranslatePage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/VotePage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Voter.php (modified) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.css (modified) (history)

Diff [purge]

Index: trunk/extensions/SecurePoll/auth-api.php
@@ -37,6 +37,8 @@
3838 echo serialize( Status::newFatal( 'securepoll-api-token-mismatch' ) );
3939 exit;
4040 }
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 ) );
4244 echo serialize( $status );
4345
Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php
@@ -151,6 +151,8 @@
152152 'securepoll-tally-upload-submit' => 'Create tally',
153153 'securepoll-tally-error' => 'Error interpreting vote record, cannot produce a tally.',
154154 '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',
155157 );
156158
157159 /** Message documentation (Message documentation)
Index: trunk/extensions/SecurePoll/SecurePoll.php
@@ -50,7 +50,6 @@
5151 $wgSpecialPages['SecurePoll'] = 'SecurePoll_BasePage';
5252
5353 $wgAutoloadClasses = $wgAutoloadClasses + array(
54 - 'SecurePoll' => "$dir/includes/Base.php",
5554 'SecurePoll_Auth' => "$dir/includes/Auth.php",
5655 'SecurePoll_LocalAuth' => "$dir/includes/Auth.php",
5756 'SecurePoll_RemoteMWAuth' => "$dir/includes/Auth.php",
@@ -58,11 +57,13 @@
5958 'SecurePoll_BasePage' => "$dir/includes/Base.php",
6059 'SecurePoll_ChooseBallot' => "$dir/includes/Ballot.php",
6160 'SecurePoll_PreferentialBallot' => "$dir/includes/Ballot.php",
 61+ 'SecurePoll_Context' => "$dir/includes/Context.php",
6262 'SecurePoll_Crypt' => "$dir/includes/Crypt.php",
6363 'SecurePoll_GpgCrypt' => "$dir/includes/Crypt.php",
6464 'SecurePoll_DetailsPage' => "$dir/includes/DetailsPage.php",
6565 'SecurePoll_DumpPage' => "$dir/includes/DumpPage.php",
6666 'SecurePoll_Election' => "$dir/includes/Election.php",
 67+ 'SecurePoll_ElectionTallier' => "$dir/includes/ElectionTallier.php",
6768 'SecurePoll_Entity' => "$dir/includes/Entity.php",
6869 'SecurePoll_EntryPage' => "$dir/includes/EntryPage.php",
6970 'SecurePoll_ListPage' => "$dir/includes/ListPage.php",
@@ -72,6 +73,10 @@
7374 'SecurePoll_Page' => "$dir/includes/Page.php",
7475 'SecurePoll_Question' => "$dir/includes/Question.php",
7576 '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",
7681 'SecurePoll_Tallier' => "$dir/includes/Tallier.php",
7782 'SecurePoll_PluralityTallier' => "$dir/includes/Tallier.php",
7883 'SecurePoll_TallyPage' => "$dir/includes/TallyPage.php",
Index: trunk/extensions/SecurePoll/includes/Voter.php
@@ -13,7 +13,8 @@
1414 /**
1515 * Create a voter from the given associative array of parameters
1616 */
17 - function __construct( $params ) {
 17+ function __construct( $context, $params ) {
 18+ $this->context = $context;
1819 foreach ( self::$paramNames as $name ) {
1920 if ( isset( $params[$name] ) ) {
2021 $this->$name = $params[$name];
@@ -25,20 +26,20 @@
2627 * Create a voter object from the database
2728 * @return SecurePoll_Voter or false if the ID is not valid
2829 */
29 - static function newFromId( $id ) {
30 - $db = wfGetDB( DB_MASTER );
 30+ static function newFromId( $context, $id ) {
 31+ $db = $context->getDB();
3132 $row = $db->selectRow( 'securepoll_voters', '*', array( 'voter_id' => $id ), __METHOD__ );
3233 if ( !$row ) {
3334 return false;
3435 }
35 - return self::newFromRow( $row );
 36+ return self::newFromRow( $context, $row );
3637 }
3738
3839 /**
3940 * Create a voter from a DB result row
4041 */
41 - static function newFromRow( $row ) {
42 - return new self( array(
 42+ static function newFromRow( $context, $row ) {
 43+ return new self( $context, array(
4344 'id' => $row->voter_id,
4445 'electionId' => $row->voter_election,
4546 'name' => $row->voter_name,
@@ -56,8 +57,8 @@
5758 * The row needs to be locked before this function is called, to avoid
5859 * duplicate key errors.
5960 */
60 - static function createVoter( $params ) {
61 - $db = wfGetDB( DB_MASTER );
 61+ static function createVoter( $context, $params ) {
 62+ $db = $context->getDB();
6263 $id = $db->nextSequenceValue( 'voters_voter_id' );
6364 $row = array(
6465 'voter_id' => $id,
@@ -70,7 +71,7 @@
7172 );
7273 $db->insert( 'securepoll_voters', $row, __METHOD__ );
7374 $params['id'] = $db->insertId();
74 - return new self( $params );
 75+ return new self( $context, $params );
7576 }
7677
7778 /** Get the voter ID */
@@ -152,7 +153,7 @@
153154 if ( isset( $_COOKIE[$cookieName] ) ) {
154155 $otherVoterId = intval( $_COOKIE[$cookieName] );
155156 if ( $otherVoterId != $this->getId() ) {
156 - $otherVoter = self::newFromId( $otherVoterId );
 157+ $otherVoter = self::newFromId( $this->context, $otherVoterId );
157158 if ( $otherVoter->getElectionId() == $this->getElectionId() ) {
158159 $this->addCookieDup( $otherVoterId );
159160 }
@@ -166,7 +167,7 @@
167168 * Flag a duplicate voter
168169 */
169170 function addCookieDup( $voterId ) {
170 - $dbw = wfGetDB( DB_MASTER );
 171+ $dbw = $this->context->getDB();
171172 # Insert the log record
172173 $dbw->insert( 'securepoll_cookie_match',
173174 array(
Index: trunk/extensions/SecurePoll/includes/Auth.php
@@ -4,6 +4,8 @@
55 * Class for handling guest logins and sessions. Creates SecurePoll_Voter objects.
66 */
77 class SecurePoll_Auth {
 8+ var $context;
 9+
810 /**
911 * List of available authorisation modules (subclasses)
1012 */
@@ -16,14 +18,18 @@
1719 * Create an auth object of the given type
1820 * @param $type string
1921 */
20 - static function factory( $type ) {
 22+ static function factory( $context, $type ) {
2123 if ( !isset( self::$authTypes[$type] ) ) {
2224 throw new MWException( "Invalid authentication type: $type" );
2325 }
2426 $class = self::$authTypes[$type];
25 - return new $class;
 27+ return new $class( $context );
2628 }
2729
 30+ function __construct( $context ) {
 31+ $this->context = $context;
 32+ }
 33+
2834 /**
2935 * Create a voter transparently, without user interaction.
3036 * Sessions authorised against local accounts are created this way.
@@ -68,7 +74,7 @@
6975 }
7076
7177 # Sanity check election ID
72 - $voter = SecurePoll_Voter::newFromId( $voterId );
 78+ $voter = SecurePoll_Voter::newFromId( $this->context, $voterId );
7379 if ( !$voter || $voter->getElectionId() != $election->getId() ) {
7480 return false;
7581 } else {
@@ -87,7 +93,7 @@
8894 * @return SecurePoll_Voter
8995 */
9096 function getVoter( $params ) {
91 - $dbw = wfGetDB( DB_MASTER );
 97+ $dbw = $this->context->getDB();
9298
9399 # This needs to be protected by FOR UPDATE
94100 # Otherwise a race condition could lead to duplicate users for a single remote user,
@@ -107,10 +113,10 @@
108114 if ( $row ) {
109115 # No need to hold the lock longer
110116 $dbw->commit();
111 - $user = SecurePoll_Voter::newFromRow( $row );
 117+ $user = SecurePoll_Voter::newFromRow( $this->context, $row );
112118 } else {
113119 # Lock needs to be held until the row is inserted
114 - $user = SecurePoll_Voter::createVoter( $params );
 120+ $user = SecurePoll_Voter::createVoter( $this->context, $params );
115121 $dbw->commit();
116122 }
117123 return $user;
@@ -178,7 +184,7 @@
179185 if ( $wgUser->isAnon() ) {
180186 return Status::newFatal( 'securepoll-not-logged-in' );
181187 }
182 - $params = self::getUserParams( $wgUser );
 188+ $params = $this->getUserParams( $wgUser );
183189 $params['electionId'] = $election->getId();
184190 $qualStatus = $election->getQualifiedStatus( $params );
185191 if ( !$qualStatus->isOK() ) {
@@ -193,7 +199,7 @@
194200 * @param $user User
195201 * @return array
196202 */
197 - static function getUserParams( $user ) {
 203+ function getUserParams( $user ) {
198204 global $wgServer;
199205 return array(
200206 'name' => $user->getName(),
@@ -207,7 +213,7 @@
208214 'bot' => $user->isBot(),
209215 'language' => $user->getOption( 'language' ),
210216 'groups' => $user->getGroups(),
211 - 'lists' => self::getLists( $user )
 217+ 'lists' => $this->getLists( $user )
212218 )
213219 );
214220 }
@@ -217,8 +223,8 @@
218224 * @param $user User
219225 * @return array
220226 */
221 - static function getLists( $user ) {
222 - $dbr = wfGetDB( DB_SLAVE );
 227+ function getLists( $user ) {
 228+ $dbr = $this->context->getDB();
223229 $res = $dbr->select(
224230 'securepoll_lists',
225231 array( 'li_name' ),
Index: trunk/extensions/SecurePoll/includes/Crypt.php
@@ -29,9 +29,9 @@
3030 * Create an encryption object of the given type. Currently only "gpg" is
3131 * implemented.
3232 */
33 - static function factory( $type, $election ) {
 33+ static function factory( $context, $type, $election ) {
3434 if ( $type === 'gpg' ) {
35 - return new SecurePoll_GpgCrypt( $election );
 35+ return new SecurePoll_GpgCrypt( $context, $election );
3636 } else {
3737 return false;
3838 }
@@ -50,14 +50,15 @@
5151 * gpg-decrypt-key is for tallying.
5252 */
5353 class SecurePoll_GpgCrypt {
54 - var $election;
 54+ var $context, $election;
5555 var $recipient, $signer, $homeDir;
5656
5757 /**
5858 * Constructor.
5959 * @param $election SecurePoll_Election
6060 */
61 - function __construct( $election ) {
 61+ function __construct( $context, $election ) {
 62+ $this->context = $context;
6263 $this->election = $election;
6364 }
6465
Index: trunk/extensions/SecurePoll/includes/DumpPage.php
@@ -19,7 +19,7 @@
2020 }
2121
2222 $electionId = intval( $params[0] );
23 - $this->election = SecurePoll::getElection( $electionId );
 23+ $this->election = $this->context->getElection( $electionId );
2424 if ( !$this->election ) {
2525 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2626 return;
@@ -66,12 +66,12 @@
6767
6868 $this->headersSent = true;
6969 $wgOut->disable();
70 - header( 'Content-Type: text/plain' );
 70+ header( 'Content-Type: application/vnd.mediawiki.securepoll' );
7171 $electionId = $this->election->getId();
72 - $filename = urlencode( "SecurePoll-$electionId-" . wfTimestampNow() );
 72+ $filename = urlencode( "$electionId-" . wfTimestampNow() . '.securepoll' );
7373 header( "Content-Disposition: attachment; filename=$filename" );
7474 echo "<SecurePoll>\n<election>\n" .
7575 $this->election->getConfXml();
76 - SecurePoll_Entity::setLanguages( array( $this->election->getLanguage() ) );
 76+ $this->context->setLanguages( array( $this->election->getLanguage() ) );
7777 }
7878 }
Index: trunk/extensions/SecurePoll/includes/Election.php
@@ -58,31 +58,22 @@
5959 /**
6060 * Constructor.
6161 *
62 - * Do not use this constructor directly, instead use SecurePoll::getElection().
 62+ * Do not use this constructor directly, instead use SecurePoll_Context::getElection().
6363 *
6464 * @param $id integer
6565 */
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'];
6875 }
6976
7077 /**
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 - /**
8778 * Get a list of localisable message names. See SecurePoll_Entity.
8879 */
8980 function getMessageNames() {
@@ -134,7 +125,7 @@
135126 */
136127 function getBallot() {
137128 if ( !$this->ballot ) {
138 - $this->ballot = SecurePoll_Ballot::factory( $this->ballotType, $this );
 129+ $this->ballot = $this->context->newBallot( $this->ballotType, $this );
139130 }
140131 return $this->ballot;
141132 }
@@ -190,7 +181,7 @@
191182 * Returns true if the user is an admin of the current election.
192183 * @param $user User
193184 */
194 - function isAdmin( User $user ) {
 185+ function isAdmin( $user ) {
195186 $admins = array_map( 'trim', explode( '|', $this->getProperty( 'admins' ) ) );
196187 return in_array( $user->getName(), $admins );
197188 }
@@ -200,7 +191,7 @@
201192 * @param $voter SecurePoll_Voter
202193 */
203194 function hasVoted( $voter ) {
204 - $db = wfGetDB( DB_MASTER );
 195+ $db = $this->context->getDB();
205196 $row = $db->selectRow(
206197 'securepoll_votes',
207198 array( "1" ),
@@ -227,33 +218,11 @@
228219 */
229220 function getQuestions() {
230221 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() );
243223 $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 );
254226 }
255 - if ( $questionId !== false ) {
256 - $this->questions[] = new SecurePoll_Question( $questionId, $options );
257 - }
258227 }
259228 return $this->questions;
260229 }
@@ -264,7 +233,7 @@
265234 */
266235 function getAuth() {
267236 if ( !$this->auth ) {
268 - $this->auth = SecurePoll_Auth::factory( $this->authType );
 237+ $this->auth = $this->context->newAuth( $this->authType );
269238 }
270239 return $this->auth;
271240 }
@@ -288,7 +257,7 @@
289258 if ( $type === false || $type === 'none' ) {
290259 return false;
291260 }
292 - $crypt = SecurePoll_Crypt::factory( $type, $this );
 261+ $crypt = $this->context->newCrypt( $type, $this );
293262 if ( !$crypt ) {
294263 throw new MWException( 'Invalid encryption type' );
295264 }
@@ -296,19 +265,10 @@
297266 }
298267
299268 /**
300 - * Get the tallier objects
301 - * @return SecurePoll_Tallier
 269+ * Get the tally type
302270 */
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;
313273 }
314274
315275 /**
@@ -319,12 +279,12 @@
320280 return Status::newFatal( 'securepoll-dump-no-crypt' );
321281 }
322282
323 - $random = SecurePoll::getRandom();
 283+ $random = $this->context->getRandom();
324284 $status = $random->open();
325285 if ( !$status->isOK() ) {
326286 return $status;
327287 }
328 - $db = wfGetDB( DB_SLAVE );
 288+ $db = $this->context->getDB();
329289 $res = $db->select(
330290 'securepoll_votes',
331291 array( '*' ),
@@ -354,7 +314,7 @@
355315 Xml::element( 'title', array(), $this->title ) . "\n" .
356316 Xml::element( 'ballot', array(), $this->ballotType ) . "\n" .
357317 Xml::element( 'tally', array(), $this->tallyType ) . "\n" .
358 - Xml::element( 'lang', array(), $this->primaryLang ) . "\n" .
 318+ Xml::element( 'primaryLang', array(), $this->primaryLang ) . "\n" .
359319 Xml::element( 'startDate', array(), wfTimestamp( TS_ISO_8601, $this->startDate ) ) . "\n" .
360320 Xml::element( 'endDate', array(), wfTimestamp( TS_ISO_8601, $this->endDate ) ) . "\n" .
361321 Xml::element( 'auth', array(), $this->authType ) . "\n" .
@@ -365,5 +325,20 @@
366326 $s .= "</configuration>\n";
367327 return $s;
368328 }
 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+ }
369344 }
370345
Index: trunk/extensions/SecurePoll/includes/LoginPage.php
@@ -14,7 +14,7 @@
1515 }
1616
1717 $electionId = intval( $params[0] );
18 - $this->election = SecurePoll::getElection( $electionId );
 18+ $this->election = $this->context->getElection( $electionId );
1919 if ( !$this->election ) {
2020 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2121 return;
Index: trunk/extensions/SecurePoll/includes/MessageDumpPage.php
@@ -10,7 +10,7 @@
1111 }
1212
1313 $electionId = intval( $params[0] );
14 - $this->election = SecurePoll::getElection( $electionId );
 14+ $this->election = $this->context->getElection( $electionId );
1515 if ( !$this->election ) {
1616 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
1717 return;
@@ -20,7 +20,7 @@
2121 header( 'Content-Type: application/x-sql; charset=utf-8' );
2222 $filename = urlencode( "sp-msgs-$electionId-" . wfTimestampNow() . '.sql' );
2323 header( "Content-Disposition: attachment; filename=$filename" );
24 - $dbr = wfGetDB( DB_SLAVE );
 24+ $dbr = $this->context->getDB();
2525
2626 $entities = array_merge( array( $this->election ), $this->election->getDescendants() );
2727 $ids = array();
Index: trunk/extensions/SecurePoll/includes/Question.php
@@ -9,12 +9,15 @@
1010
1111 /**
1212 * 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
1515 */
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+ }
1922 }
2023
2124 /**
Index: trunk/extensions/SecurePoll/includes/Option.php
@@ -6,22 +6,15 @@
77 */
88 class SecurePoll_Option extends SecurePoll_Entity {
99 /**
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
1213 */
13 - static function newFromRow( $row ) {
14 - return new self( $row->op_entity );
 14+ function __construct( $context, $info ) {
 15+ parent::__construct( $context, 'option', $info );
1516 }
1617
1718 /**
18 - * Constructor, from entity ID
19 - * @param $id integer
20 - */
21 - function __construct( $id ) {
22 - parent::__construct( 'option', $id );
23 - }
24 -
25 - /**
2619 * Get a list of localisable message names. This is used to provide the
2720 * translate subpage with a list of messages to localise.
2821 */
Index: trunk/extensions/SecurePoll/includes/ListPage.php
@@ -20,7 +20,7 @@
2121 }
2222
2323 $electionId = intval( $params[0] );
24 - $this->election = SecurePoll::getElection( $electionId );
 24+ $this->election = $this->context->getElection( $electionId );
2525 if ( !$this->election ) {
2626 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2727 return;
@@ -84,7 +84,7 @@
8585 */
8686 static function ajaxStrike( $action, $id, $reason ) {
8787 wfLoadExtensionMessages( 'SecurePoll' );
88 - $db = wfGetDB( DB_MASTER );
 88+ $db = $this->context->getDB();
8989 $table = $db->tableName( 'securepoll_elections' );
9090 $row = $db->selectRow(
9191 array( 'securepoll_votes', 'securepoll_elections' ),
@@ -98,9 +98,9 @@
9999 'message' => wfMsgHtml( 'securepoll-strike-nonexistent' )
100100 ) );
101101 }
102 - $page = new SecurePollPage;
 102+ $page = new SecurePoll_BasePage;
103103 $subpage = new self( $page );
104 - $subpage->election = SecurePoll_Election::newFromRow( $row );
 104+ $subpage->election = $subpage->context->newElectionFromRow( $row );
105105 $status = $subpage->strike( $action, $id, $reason );
106106 if ( $status->isGood() ) {
107107 return Xml::encodeJsVar( (object)array( 'status' => 'good' ) );
@@ -122,7 +122,7 @@
123123 */
124124 function strike( $action, $voteId, $reason ) {
125125 global $wgUser;
126 - $dbw = wfGetDB( DB_MASTER );
 126+ $dbw = $this->context->getDB();
127127 if ( !$this->election->isAdmin( $wgUser ) ) {
128128 return Status::newFatal( 'securepoll-need-admin' );
129129 }
Index: trunk/extensions/SecurePoll/includes/Base.php
@@ -1,36 +1,5 @@
22 <?php
33
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 -
354 class SecurePoll_BasePage extends UnlistedSpecialPage {
365 static $pages = array(
376 'details' => 'SecurePoll_DetailsPage',
@@ -44,11 +13,14 @@
4514 'vote' => 'SecurePoll_VotePage',
4615 );
4716
 17+ var $sp_context;
 18+
4819 /**
4920 * Constructor
5021 */
5122 public function __construct() {
5223 parent::__construct( 'SecurePoll' );
 24+ $this->sp_context = new SecurePoll_Context;
5325 }
5426
5527 /**
@@ -57,7 +29,7 @@
5830 * @param $paramString Mixed: parameter passed to the page or null
5931 */
6032 public function execute( $paramString ) {
61 - global $wgOut, $wgRequest, $wgScriptPath;
 33+ global $wgOut, $wgUser, $wgRequest, $wgScriptPath;
6234
6335 wfLoadExtensionMessages( 'SecurePoll' );
6436
@@ -83,9 +55,16 @@
8456 return;
8557 }
8658
 59+ if ( !($page instanceof SecurePoll_EntryPage ) ) {
 60+ $this->setSubtitle();
 61+ }
 62+
8763 $page->execute( $params );
8864 }
8965
 66+ /**
 67+ * Get a SecurePoll_Page subclass object for the given subpage name
 68+ */
9069 function getSubpage( $name ) {
9170 if ( !isset( self::$pages[$name] ) ) {
9271 return false;
@@ -95,10 +74,32 @@
9675 return $page;
9776 }
9877
 78+ /**
 79+ * Get a random token for CSRF protection
 80+ */
9981 function getEditToken() {
10082 if ( !isset( $_SESSION['spToken'] ) ) {
10183 $_SESSION['spToken'] = sha1( mt_rand() . mt_rand() . mt_rand() );
10284 }
10385 return $_SESSION['spToken'];
10486 }
 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 = '&lt; ' . $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+ }
105106 }
Index: trunk/extensions/SecurePoll/includes/VotePage.php
@@ -19,7 +19,7 @@
2020 }
2121
2222 $electionId = intval( $params[0] );
23 - $this->election = SecurePoll::getElection( $electionId );
 23+ $this->election = $this->context->getElection( $electionId );
2424 if ( !$this->election ) {
2525 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2626 return;
@@ -160,7 +160,7 @@
161161 $encrypted = $status->value;
162162 }
163163
164 - $dbw = wfGetDB( DB_MASTER );
 164+ $dbw = $this->context->getDB();
165165 $dbw->begin();
166166
167167 # Mark previous votes as old
Index: trunk/extensions/SecurePoll/includes/Page.php
@@ -5,6 +5,7 @@
66 */
77 abstract class SecurePoll_Page {
88 var $parent, $election, $auth, $user;
 9+ var $context;
910
1011 /**
1112 * Constructor.
@@ -12,6 +13,7 @@
1314 */
1415 function __construct( $parent ) {
1516 $this->parent = $parent;
 17+ $this->context = $parent->sp_context;
1618 }
1719
1820 /**
@@ -48,6 +50,6 @@
4951 if ( $fallback != 'en' ) {
5052 $languages[] = 'en';
5153 }
52 - SecurePoll_Entity::setLanguages( $languages );
 54+ $this->context->setLanguages( $languages );
5355 }
5456 }
Index: trunk/extensions/SecurePoll/includes/Entity.php
@@ -12,22 +12,21 @@
1313 */
1414 class SecurePoll_Entity {
1515 var $id;
 16+ var $context;
1617 var $messagesLoaded = array();
1718 var $properties;
1819
19 - static $languages = array( 'en' );
20 - static $messageCache = array();
21 - static $parserOptions;
22 -
2320 /**
2421 * Create an entity of the given type. This is typically called from the
2522 * child constructor.
 23+ * @param $context SecurePoll_Context
2624 * @param $type string
27 - * @param $id integer
 25+ * @param $info Associative array of entity info
2826 */
29 - function __construct( $type, $id = false ) {
 27+ function __construct( $context, $type, $info ) {
 28+ $this->context = $context;
3029 $this->type = $type;
31 - $this->id = $id;
 30+ $this->id = isset( $info['id'] ) ? $info['id'] : false;
3231 }
3332
3433 /**
@@ -55,17 +54,6 @@
5655 }
5756
5857 /**
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 - /**
7058 * Get the child entity objects. When the messages of an object are loaded,
7159 * the messages of the children are loaded automatically, to reduce the
7260 * query count.
@@ -97,32 +85,13 @@
9886 */
9987 function loadMessages( $lang = false ) {
10088 if ( $lang === false ) {
101 - $lang = reset( self::$languages );
 89+ $lang = reset( $this->context->languages );
10290 }
10391 $ids = array( $this->getId() );
10492 foreach ( $this->getDescendants() as $child ) {
105 - $id = $child->getId();
106 - if ( !isset( self::$messageCache[$lang][$id] ) ) {
107 - $ids[] = $id;
108 - }
 93+ $ids[] = $child->getId();
10994 }
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 );
12796 $this->messagesLoaded[$lang] = true;
12897 }
12998
@@ -132,15 +101,11 @@
133102 * automatically.
134103 */
135104 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();
145110 }
146111 }
147112
@@ -155,11 +120,7 @@
156121 if ( empty( $this->messagesLoaded[$language] ) ) {
157122 $this->loadMessages( $language );
158123 }
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 );
164125 }
165126
166127 /**
@@ -171,12 +132,13 @@
172133 */
173134 function getMessage( $name ) {
174135 $id = $this->getId();
175 - foreach ( self::$languages as $language ) {
 136+ foreach ( $this->context->languages as $language ) {
176137 if ( empty( $this->messagesLoaded[$language] ) ) {
177138 $this->loadMessages( $language );
178139 }
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;
181143 }
182144 }
183145 return "[$name]";
@@ -187,16 +149,14 @@
188150 */
189151 function parseMessage( $name, $lineStart = true ) {
190152 global $wgParser, $wgTitle;
191 - if ( !self::$parserOptions ) {
192 - self::$parserOptions = new ParserOptions;
193 - }
 153+ $parserOptions = $this->context->getParserOptions();
194154 if ( $wgTitle ) {
195155 $title = $wgTitle;
196156 } else {
197157 $title = SpecialPage::getTitleFor( 'SecurePoll' );
198158 }
199159 $wikiText = $this->getMessage( $name );
200 - $out = $wgParser->parse( $wikiText, $title, self::$parserOptions, $lineStart );
 160+ $out = $wgParser->parse( $wikiText, $title, $parserOptions, $lineStart );
201161 return $out->getText();
202162 }
203163
@@ -252,7 +212,7 @@
253213 $s .= Xml::element( 'property', array( 'name' => $name ), $value ) . "\n";
254214 }
255215 foreach ( $this->getMessageNames() as $name ) {
256 - foreach ( self::$languages as $lang ) {
 216+ foreach ( $this->context->languages as $lang ) {
257217 $s .= Xml::element( 'message', array( 'name' => $name, 'lang' => $lang ),
258218 $this->getRawMessage( $name, $lang ) ) . "\n";
259219 }
Index: trunk/extensions/SecurePoll/includes/Ballot.php
@@ -4,7 +4,7 @@
55 * Parent class for ballot forms. This is the UI component of a voting method.
66 */
77 abstract class SecurePoll_Ballot {
8 - var $election;
 8+ var $election, $context;
99
1010 /**
1111 * Get a list of names of tallying methods, which may be used to produce a
@@ -35,17 +35,18 @@
3636
3737 /**
3838 * Create a ballot of the given type
 39+ * @param $context SecurePoll_Context
3940 * @param $type string
4041 * @param $election SecurePoll_Election
4142 */
42 - static function factory( $type, $election ) {
 43+ static function factory( $context, $type, $election ) {
4344 switch ( $type ) {
4445 case 'approval':
45 - return new SecurePoll_ApprovalBallot( $election );
 46+ return new SecurePoll_ApprovalBallot( $context, $election );
4647 case 'preferential':
47 - return new SecurePoll_PreferentialBallot( $election );
 48+ return new SecurePoll_PreferentialBallot( $context, $election );
4849 case 'choose':
49 - return new SecurePoll_ChooseBallot( $election );
 50+ return new SecurePoll_ChooseBallot( $context, $election );
5051 default:
5152 throw new MWException( "Invalid ballot type: $type" );
5253 }
@@ -53,9 +54,11 @@
5455
5556 /**
5657 * Constructor.
 58+ * @param $context SecurePoll_Context
5759 * @param $election SecurePoll_Election
5860 */
59 - function __construct( $election ) {
 61+ function __construct( $context, $election ) {
 62+ $this->context = $context;
6063 $this->election = $election;
6164 }
6265
@@ -116,10 +119,12 @@
117120 $optionHTML = $option->parseMessageInline( 'text' );
118121 $optionId = $option->getId();
119122 $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 ) ) .
121126 '&nbsp;' .
122127 Xml::tags( 'label', array( 'for' => $radioId ), $optionHTML ) .
123 - "<br/>\n";
 128+ "</div>\n";
124129 }
125130 return $s;
126131 }
@@ -189,6 +194,7 @@
190195 $inputId = "{$name}_opt{$optionId}";
191196 $oldValue = $wgRequest->getVal( $inputId, '' );
192197 $s .=
 198+ '<div class="securepoll-option-preferential">' .
193199 Xml::input( $inputId, '3', $oldValue, array(
194200 'id' => $inputId,
195201 'maxlength' => 3,
@@ -196,7 +202,7 @@
197203 '&nbsp;' .
198204 Xml::tags( 'label', array( 'for' => $inputId ), $optionHTML ) .
199205 '&nbsp;' .
200 - "<br/>\n";
 206+ "</div>\n";
201207 }
202208 return $s;
203209 }
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
1121 + native
Index: trunk/extensions/SecurePoll/includes/TallyPage.php
@@ -17,14 +17,13 @@
1818 }
1919
2020 $electionId = intval( $params[0] );
21 - $this->election = SecurePoll::getElection( $electionId );
 21+ $this->election = $this->context->getElection( $electionId );
2222 if ( !$this->election ) {
2323 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2424 return;
2525 }
2626 $this->initLanguage( $wgUser, $this->election );
2727 $wgOut->setPageTitle( wfMsg( 'securepoll-tally-title', $this->election->getMessage( 'title' ) ) );
28 -
2928 if ( !$this->election->isAdmin( $wgUser ) ) {
3029 $wgOut->addWikiMsg( 'securepoll-need-admin' );
3130 return;
@@ -125,57 +124,13 @@
126125 */
127126 function submitLocal() {
128127 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;
166132 }
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() );
180135 }
181136
182137 /**
@@ -183,8 +138,6 @@
184139 */
185140 function submitUpload() {
186141 global $wgOut;
187 - $crypt = $this->election->getCrypt();
188 - $tallier = $this->election->getTallier();
189142 if ( !isset( $_FILES['tally_file'] )
190143 || !is_uploaded_file( $_FILES['tally_file']['tmp_name'] )
191144 || !$_FILES['tally_file']['size'] )
@@ -192,24 +145,20 @@
193146 $wgOut->addWikiMsg( 'securepoll-no-upload' );
194147 return;
195148 }
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;
212153 }
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;
214163 $wgOut->addHTML( $tallier->getHtmlResult() );
215164 }
216165
Index: trunk/extensions/SecurePoll/includes/DetailsPage.php
@@ -17,7 +17,8 @@
1818 }
1919
2020 $this->voteId = intval( $params[0] );
21 - $db = wfGetDB( DB_MASTER );
 21+
 22+ $db = $this->context->getDB();
2223 $row = $db->selectRow(
2324 array( 'securepoll_votes', 'securepoll_elections', 'securepoll_voters' ),
2425 '*',
@@ -33,8 +34,13 @@
3435 return;
3536 }
3637
37 - $this->election = SecurePoll_Election::newFromRow( $row );
 38+ $this->election = $this->context->newElectionFromRow( $row );
3839 $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+
3945 if ( !$this->election->isAdmin( $wgUser ) ) {
4046 $wgOut->addWikiMsg( 'securepoll-need-admin' );
4147 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
1281 + native
Index: trunk/extensions/SecurePoll/includes/TranslatePage.php
@@ -17,7 +17,7 @@
1818 }
1919
2020 $electionId = intval( $params[0] );
21 - $this->election = SecurePoll::getElection( $electionId );
 21+ $this->election = $this->context->getElection( $electionId );
2222 if ( !$this->election ) {
2323 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2424 return;
@@ -53,6 +53,11 @@
5454 return;
5555 }
5656
 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+
5762 # If the request was posted, do the submit
5863 if ( $wgRequest->wasPosted() && $wgRequest->getVal( 'action' ) == 'submit' ) {
5964 $this->doSubmit( $secondary );
@@ -102,7 +107,7 @@
103108 /**
104109 * @return Title
105110 */
106 - function getTitle( $lang ) {
 111+ function getTitle( $lang = false ) {
107112 $subpage = 'translate/' . $this->election->getId();
108113 if ( $lang !== false ) {
109114 $subpage .= '/' . $lang;
@@ -166,7 +171,7 @@
167172 }
168173 }
169174 if ( $replaceBatch ) {
170 - $dbw = wfGetDB( DB_MASTER );
 175+ $dbw = $this->context->getDB();
171176 $dbw->replace(
172177 'securepoll_msgs',
173178 array( array( 'msg_entity', 'msg_lang', 'msg_key' ) ),
Index: trunk/extensions/SecurePoll/includes/Tallier.php
@@ -1,7 +1,7 @@
22 <?php
33
44 abstract class SecurePoll_Tallier {
5 - var $question;
 5+ var $context, $question;
66
77 abstract function addVote( $scores );
88 abstract function getHtmlResult();
@@ -9,18 +9,19 @@
1010
1111 abstract function finishTally();
1212
13 - static function factory( $type, $question ) {
 13+ static function factory( $context, $type, $question ) {
1414 switch ( $type ) {
1515 case 'plurality':
16 - return new SecurePoll_PluralityTallier( $question );
 16+ return new SecurePoll_PluralityTallier( $context, $question );
1717 case 'schulze':
18 - return new SecurePoll_SchulzeTallier( $question );
 18+ return new SecurePoll_SchulzeTallier( $context, $question );
1919 default:
2020 throw new MWException( "Invalid tallier type: $type" );
2121 }
2222 }
2323
24 - function __construct( $question ) {
 24+ function __construct( $context, $question ) {
 25+ $this->context = $context;
2526 $this->question = $question;
2627 }
2728 }
@@ -31,8 +32,8 @@
3233 class SecurePoll_PluralityTallier extends SecurePoll_Tallier {
3334 var $tally = array();
3435
35 - function __construct( $question ) {
36 - parent::__construct( $question );
 36+ function __construct( $context, $question ) {
 37+ parent::__construct( $context, $question );
3738 foreach ( $question->getOptions() as $option ) {
3839 $this->tally[$option->getId()] = 0;
3940 }
@@ -56,7 +57,7 @@
5758
5859 function getHtmlResult() {
5960 // Show the results
60 - $s = '';
 61+ $s = "<table class=\"securepoll-results\">\n";
6162
6263 foreach ( $this->question->getOptions() as $option ) {
6364 $s .= '<tr><td>' . $option->getMessage( 'text' ) . "</td>\n" .
@@ -70,7 +71,7 @@
7172 function getTextResult() {
7273 // Calculate column width
7374 $width = 10;
74 - foreach ( $question->getOptions() as $option ) {
 75+ foreach ( $this->question->getOptions() as $option ) {
7576 $width = max( $width, strlen( $option->getMessage( 'text' ) ) );
7677 }
7778 if ( $width > 57 ) {
@@ -78,8 +79,12 @@
7980 }
8081
8182 // 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 ) {
8489 $otext = $option->getMessage( 'text' );
8590 if ( strlen( $otext ) > $width ) {
8691 $otext = substr( $otext, 0, $width - 3 ) . '...';
@@ -87,7 +92,7 @@
8893 $otext = str_pad( $otext, $width );
8994 }
9095 $s .= $otext . ' | ' .
91 - $this->tally[$question->getId()][$option->getId()] . "\n";
 96+ $this->tally[$option->getId()] . "\n";
9297 }
9398 return $s;
9499 }
@@ -111,8 +116,8 @@
112117 var $optionIds = array();
113118 var $victories = array();
114119
115 - function __construct( $question ) {
116 - parent::__construct( $question );
 120+ function __construct( $context, $question ) {
 121+ parent::__construct( $context, $question );
117122 $this->optionIds = array();
118123 foreach ( $question->getOptions() as $option ) {
119124 $this->optionIds[] = $option->getId();
@@ -218,7 +223,6 @@
219224
220225 function getHtmlResult() {
221226 return '<pre>' . $this->getTextResult() . '</pre>';
222 -
223227 }
224228
225229 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
1551 + native
Index: trunk/extensions/SecurePoll/cli/dump.php
@@ -12,7 +12,8 @@
1313 spFatal( "Usage: php dump.php [-o <outfile>] <election name>" );
1414 }
1515
16 -$election = SecurePoll::getElectionByTitle( $args[0] );
 16+$context = new SecurePoll_Context;
 17+$election = $context->getElectionByTitle( $args[0] );
1718 if ( !$election ) {
1819 spFatal( "There is no election called \"$args[0]\"" );
1920 }
@@ -23,7 +24,7 @@
2425 $fileName = $options['o'];
2526 }
2627 if ( $fileName === '-' ) {
27 - $outFile = STDIN;
 28+ $outFile = STDOUT;
2829 } else {
2930 $outFile = fopen( $fileName, 'w' );
3031 }
@@ -31,7 +32,7 @@
3233 spFatal( "Unable to open $fileName for writing" );
3334 }
3435
35 -SecurePoll_Entity::setLanguages( array( $election->getLanguage() ) );
 36+$context->setLanguages( array( $election->getLanguage() ) );
3637
3738 $cbdata = array(
3839 'header' => "<SecurePoll>\n<election>\n" . $election->getConfXml(),
@@ -45,7 +46,7 @@
4647 spFatal( $status->getWikiText() );
4748 }
4849 if ( $election->cbdata['header'] ) {
49 - echo $election->cbdata['header'];
 50+ fwrite( $outFile, $election->cbdata['header'] );
5051 }
5152
5253 fwrite( $outFile, "</election>\n</SecurePoll>\n" );
@@ -57,7 +58,7 @@
5859
5960 function spDumpVote( $election, $row ) {
6061 if ( $election->cbdata['header'] ) {
61 - echo $election->cbdata['header'];
 62+ fwrite( $election->cbdata['outFile'], $election->cbdata['header'] );
6263 $election->cbdata['header'] = false;
6364 }
6465 fwrite( $election->cbdata['outFile'], "<vote>" . $row->vote_record . "</vote>\n" );
Index: trunk/extensions/SecurePoll/cli/tally.php
@@ -5,10 +5,6 @@
66 *
77 * Can be used to tally very large numbers of votes, when the web interface is
88 * 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.
139 */
1410
1511 $optionsWithArgs = array( 'name' );
@@ -18,90 +14,49 @@
1915 Usage:
2016 php tally.php [--html] --name <election name>
2117 php tally.php [--html] <dump file>
22 -
2318 EOT;
2419
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 );
3122 }
3223
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+ }
3628 require( dirname( __FILE__ ) . '/../SecurePoll.php' );
3729 }
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 );
4530
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]}\"" );
9536 }
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]}\"" );
10740 }
 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+ }
10847 }
 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 @@
4343 border-color: red;
4444 background-color: #fff2f2;
4545 }
 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+

Status & tagging log