r53548 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r53547‎ | r53548 | r53549 >
Date:11:51, 20 July 2009
Author:tstarling
Status:deferred
Tags:
Comment:
Committing work in progress.

* Added the basic preferential ballot
* Added a first cut attempt at Schulze tallying, not ready for serious testing yet
* Added command-line scripts for dumping and tallying
* Changed the dump output format to XML. The new dump format includes configuration. Input side not done yet.
* Moved web-accessible files like CSS and JS to a resources directory
* Made Tallier objects operate only on one question at a time, to simplify the implementation of new tallying methods
* Introduced a SecurePoll class for common functions
* Moved the SpecialPage subclass to Base.php
* Increased cookie expiry time
* Added documentation to the schema file
* (bug 18966) fixed missing icon critical-32.png
* Factored out the /dev/urandom stuff to Random.php
* Removed Kwan Ting Chan from the credits since I don't think he wrote any of this extension now
* Added parseMessage utility function to Entity
* Removed error div from securepoll-bad-ballot-submission since it seems to have been added to the calling code at some point, causing a double box
Modified paths:
  • /trunk/extensions/SecurePoll/SecurePoll.css (deleted) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.i18n.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.js (deleted) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.php (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll.sql (modified) (history)
  • /trunk/extensions/SecurePoll/SecurePoll_body.php (deleted) (history)
  • /trunk/extensions/SecurePoll/cli (added) (history)
  • /trunk/extensions/SecurePoll/cli/cli.inc (added) (history)
  • /trunk/extensions/SecurePoll/cli/dump.php (added) (history)
  • /trunk/extensions/SecurePoll/cli/tally.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/Ballot.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Base.php (added) (history)
  • /trunk/extensions/SecurePoll/includes/DumpPage.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Election.php (modified) (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/Question.php (modified) (history)
  • /trunk/extensions/SecurePoll/includes/Random.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 (added) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.css (added) (history)
  • /trunk/extensions/SecurePoll/resources/SecurePoll.js (added) (history)
  • /trunk/extensions/SecurePoll/resources/critical-32.png (added) (history)

Diff [purge]

Index: trunk/extensions/SecurePoll/SecurePoll.js
@@ -1,111 +0,0 @@
2 -function securepoll_strike_popup(e, action, id) {
3 - var pop = document.getElementById('securepoll-popup');
4 -
5 - var e = window.event || e;
6 - if(!e) return;
7 - var target = e.target || e.srcElement;
8 - if(!target) return;
9 -
10 - if ( pop.parentNode.tagName.toLowerCase() != 'body' ) {
11 - pop = pop.parentNode.removeChild( pop );
12 - pop = document.body.appendChild( pop );
13 - }
14 -
15 - var left = 0;
16 - var top = 0;
17 - var containing = target;
18 - while ( containing ) {
19 - left += containing.offsetLeft;
20 - top += containing.offsetTop;
21 - containing = containing.offsetParent;
22 - }
23 - left += target.offsetWidth - 10;
24 - top += target.offsetHeight - 10;
25 -
26 - // Show the appropriate button
27 - var strikeButton = document.getElementById( 'securepoll-strike-button' );
28 - var unstrikeButton = document.getElementById( 'securepoll-unstrike-button' );
29 - if ( action == 'strike' ) {
30 - strikeButton.style.display = 'inline';
31 - strikeButton.disabled = false;
32 - unstrikeButton.style.display = 'none';
33 - unstrikeButton.disabled = true;
34 - } else {
35 - unstrikeButton.style.display = 'inline';
36 - unstrikeButton.disabled = false;
37 - strikeButton.style.display = 'none';
38 - strikeButton.disabled = true;
39 - }
40 - document.getElementById( 'securepoll-strike-result' ).innerHTML = '';
41 -
42 - // Set the hidden fields for submission
43 - document.getElementById( 'securepoll-action').value = action;
44 - document.getElementById( 'securepoll-vote-id' ).value = id;
45 -
46 - // Show the popup
47 - pop.style.left = left + 'px';
48 - pop.style.top = top + 'px';
49 - pop.style.display = 'block';
50 -
51 - // Focus on the reason box
52 - var reason = document.getElementById( 'securepoll-strike-reason' );
53 - reason.focus();
54 - reason.select();
55 -}
56 -
57 -function securepoll_strike(action) {
58 - var popup = document.getElementById('securepoll-popup');
59 - if(action == 'cancel') {
60 - popup.style.display = '';
61 - return;
62 - }
63 - if ( action == 'submit' ) {
64 - action = document.getElementById( 'securepoll-action' ).value;
65 - }
66 - var id = document.getElementById( 'securepoll-vote-id' ).value;
67 - var strikeButton = document.getElementById( 'securepoll-strike-button' );
68 - var unstrikeButton = document.getElementById( 'securepoll-unstrike-button' );
69 - var spinner = document.getElementById( 'securepoll-strike-spinner' );
70 - strikeButton.disabled = true;
71 - unstrikeButton.disabled = true;
72 - spinner.style.display = 'block';
73 - var reason = document.getElementById( 'securepoll-strike-reason' ).value;
74 -
75 - var processResult = function (xhr) {
76 - spinner.style.display = 'none';
77 - strikeButton.disabled = false;
78 - unstrikeButton.disabled = false;
79 -
80 - if ( xhr.status >= 300 || xhr.status < 200 ) {
81 - document.getElementById( 'securepoll-strike-result' ).innerHTML = xhr.responseText;
82 - return;
83 - }
84 -
85 - // Evaluate JSON result, with brackets to avoid interpretation as a code block
86 - result = eval( '(' + xhr.responseText + ')' );
87 - if ( result.status == 'good' ) {
88 - popup.style.display = 'none';
89 - } else {
90 - document.getElementById( 'securepoll-strike-result' ).innerHTML = result.message;
91 - }
92 -
93 - securepoll_modify_document( action, id );
94 - };
95 -
96 - sajax_do_call( 'wfSecurePollStrike', [ action, id, reason ], processResult );
97 -}
98 -
99 -function securepoll_modify_document( action, voteId ) {
100 - var popupButton = document.getElementById( 'securepoll-popup-' + voteId );
101 - var row = popupButton.parentNode.parentNode;
102 - if ( action == 'strike' ) {
103 - row.className += ' securepoll-struck-vote';
104 - popupButton.value = securepoll_unstrike_button;
105 - } else {
106 - row.className = row.className.replace( 'securepoll-struck-vote', '' );
107 - popupButton.value = securepoll_strike_button;
108 - }
109 - popupButton.onclick = function (event) {
110 - securepoll_strike_popup( event, action == 'strike' ? 'unstrike' : 'strike', voteId );
111 - }
112 -}
Index: trunk/extensions/SecurePoll/SecurePoll_body.php
@@ -1,86 +0,0 @@
2 -<?php
3 -if ( !defined( 'MEDIAWIKI' ) ) {
4 - die( "Not a valid entry point\n" );
5 -}
6 -
7 -class SecurePollPage extends UnlistedSpecialPage {
8 - static $pages = array(
9 - 'details' => 'SecurePoll_DetailsPage',
10 - 'dump' => 'SecurePoll_DumpPage',
11 - 'entry' => 'SecurePoll_EntryPage',
12 - 'list' => 'SecurePoll_ListPage',
13 - 'login' => 'SecurePoll_LoginPage',
14 - 'msgdump' => 'SecurePoll_MessageDumpPage',
15 - 'tally' => 'SecurePoll_TallyPage',
16 - 'translate' => 'SecurePoll_TranslatePage',
17 - 'vote' => 'SecurePoll_VotePage',
18 - );
19 -
20 - /**
21 - * Constructor
22 - */
23 - public function __construct() {
24 - parent::__construct( 'SecurePoll' );
25 - }
26 -
27 - /**
28 - * Show the special page
29 - *
30 - * @param $paramString Mixed: parameter passed to the page or null
31 - */
32 - public function execute( $paramString ) {
33 - global $wgOut, $wgRequest, $wgScriptPath;
34 -
35 - wfLoadExtensionMessages( 'SecurePoll' );
36 -
37 - $this->setHeaders();
38 - $wgOut->addLink( array(
39 - 'rel' => 'stylesheet',
40 - 'href' => "$wgScriptPath/extensions/SecurePoll/SecurePoll.css",
41 - 'type' => 'text/css'
42 - ) );
43 - $wgOut->addScriptFile( "$wgScriptPath/extensions/SecurePoll/SecurePoll.js" );
44 -
45 - $this->request = $wgRequest;
46 -
47 - $paramString = strval( $paramString );
48 - if ( $paramString === '' ) {
49 - $paramString = 'entry';
50 - }
51 - $params = explode( '/', $paramString );
52 - $pageName = array_shift( $params );
53 - $page = $this->getSubpage( $pageName );
54 - if ( !$page ) {
55 - $wgOut->addWikiMsg( 'securepoll-invalid-page', $pageName );
56 - return;
57 - }
58 -
59 - $page->execute( $params );
60 - }
61 -
62 - function getSubpage( $name ) {
63 - if ( !isset( self::$pages[$name] ) ) {
64 - return false;
65 - }
66 - $className = self::$pages[$name];
67 - $page = new $className( $this );
68 - return $page;
69 - }
70 -
71 - function getElection( $id ) {
72 - $db = wfGetDB( DB_MASTER );
73 - $row = $db->selectRow( 'securepoll_elections', '*', array( 'el_entity' => $id ), __METHOD__ );
74 - if ( $row ) {
75 - return SecurePoll_Election::newFromRow( $row );
76 - } else {
77 - return false;
78 - }
79 - }
80 -
81 - function getEditToken() {
82 - if ( !isset( $_SESSION['spToken'] ) ) {
83 - $_SESSION['spToken'] = sha1( mt_rand() . mt_rand() . mt_rand() );
84 - }
85 - return $_SESSION['spToken'];
86 - }
87 -}
Index: trunk/extensions/SecurePoll/SecurePoll.css
@@ -1,44 +0,0 @@
2 -.securepoll-old-vote {
3 - color: #666;
4 -}
5 -.securepoll-struck-vote td {
6 - text-decoration: line-through;
7 -}
8 -.securepoll-popup {
9 - padding: 0.5em 1em 0.5em 1em;
10 - border: outset;
11 - position: absolute;
12 - height: 7em;
13 - width: 38em;
14 - display: none;
15 - background-color: #ffffdd;
16 - overflow: auto;
17 - z-index: 3;
18 -}
19 -.securepoll-confirm-button {
20 - width: 16%;
21 - margin-left: 16%;
22 - margin-right: 16%;
23 -}
24 -#securepoll-strike-spinner {
25 - display: none;
26 - margin-left: auto;
27 - margin-right: auto;
28 - width: 30px;
29 -}
30 -.securepoll-detail-header {
31 - font-weight: bold;
32 - width: 20%;
33 -}
34 -.securepoll-trans-table {
35 - width: 100%;
36 -}
37 -.securepoll-error-box {
38 - font-size: larger;
39 - border: 2px solid;
40 - padding: .5em 1em;
41 - margin-bottom: 2em;
42 - color: #000;
43 - border-color: red;
44 - background-color: #fff2f2;
45 -}
Index: trunk/extensions/SecurePoll/resources/critical-32.png
Cannot display: file marked as a binary type.
svn:mime-type = image/png
Property changes on: trunk/extensions/SecurePoll/resources/critical-32.png
___________________________________________________________________
Added: svn:mime-type
461 + image/png
Added: svn:executable
472 + *
Index: trunk/extensions/SecurePoll/resources/SecurePoll.css
@@ -0,0 +1,44 @@
 2+.securepoll-old-vote {
 3+ color: #666;
 4+}
 5+.securepoll-struck-vote td {
 6+ text-decoration: line-through;
 7+}
 8+.securepoll-popup {
 9+ padding: 0.5em 1em 0.5em 1em;
 10+ border: outset;
 11+ position: absolute;
 12+ height: 7em;
 13+ width: 38em;
 14+ display: none;
 15+ background-color: #ffffdd;
 16+ overflow: auto;
 17+ z-index: 3;
 18+}
 19+.securepoll-confirm-button {
 20+ width: 16%;
 21+ margin-left: 16%;
 22+ margin-right: 16%;
 23+}
 24+#securepoll-strike-spinner {
 25+ display: none;
 26+ margin-left: auto;
 27+ margin-right: auto;
 28+ width: 30px;
 29+}
 30+.securepoll-detail-header {
 31+ font-weight: bold;
 32+ width: 20%;
 33+}
 34+.securepoll-trans-table {
 35+ width: 100%;
 36+}
 37+.securepoll-error-box {
 38+ font-size: larger;
 39+ border: 2px solid;
 40+ padding: .5em 1em;
 41+ margin-bottom: 2em;
 42+ color: #000;
 43+ border-color: red;
 44+ background-color: #fff2f2;
 45+}
Property changes on: trunk/extensions/SecurePoll/resources/SecurePoll.css
___________________________________________________________________
Added: svn:mergeinfo
Added: svn:eol-style
146 + native
Index: trunk/extensions/SecurePoll/resources/SecurePoll.js
@@ -0,0 +1,111 @@
 2+function securepoll_strike_popup(e, action, id) {
 3+ var pop = document.getElementById('securepoll-popup');
 4+
 5+ var e = window.event || e;
 6+ if(!e) return;
 7+ var target = e.target || e.srcElement;
 8+ if(!target) return;
 9+
 10+ if ( pop.parentNode.tagName.toLowerCase() != 'body' ) {
 11+ pop = pop.parentNode.removeChild( pop );
 12+ pop = document.body.appendChild( pop );
 13+ }
 14+
 15+ var left = 0;
 16+ var top = 0;
 17+ var containing = target;
 18+ while ( containing ) {
 19+ left += containing.offsetLeft;
 20+ top += containing.offsetTop;
 21+ containing = containing.offsetParent;
 22+ }
 23+ left += target.offsetWidth - 10;
 24+ top += target.offsetHeight - 10;
 25+
 26+ // Show the appropriate button
 27+ var strikeButton = document.getElementById( 'securepoll-strike-button' );
 28+ var unstrikeButton = document.getElementById( 'securepoll-unstrike-button' );
 29+ if ( action == 'strike' ) {
 30+ strikeButton.style.display = 'inline';
 31+ strikeButton.disabled = false;
 32+ unstrikeButton.style.display = 'none';
 33+ unstrikeButton.disabled = true;
 34+ } else {
 35+ unstrikeButton.style.display = 'inline';
 36+ unstrikeButton.disabled = false;
 37+ strikeButton.style.display = 'none';
 38+ strikeButton.disabled = true;
 39+ }
 40+ document.getElementById( 'securepoll-strike-result' ).innerHTML = '';
 41+
 42+ // Set the hidden fields for submission
 43+ document.getElementById( 'securepoll-action').value = action;
 44+ document.getElementById( 'securepoll-vote-id' ).value = id;
 45+
 46+ // Show the popup
 47+ pop.style.left = left + 'px';
 48+ pop.style.top = top + 'px';
 49+ pop.style.display = 'block';
 50+
 51+ // Focus on the reason box
 52+ var reason = document.getElementById( 'securepoll-strike-reason' );
 53+ reason.focus();
 54+ reason.select();
 55+}
 56+
 57+function securepoll_strike(action) {
 58+ var popup = document.getElementById('securepoll-popup');
 59+ if(action == 'cancel') {
 60+ popup.style.display = '';
 61+ return;
 62+ }
 63+ if ( action == 'submit' ) {
 64+ action = document.getElementById( 'securepoll-action' ).value;
 65+ }
 66+ var id = document.getElementById( 'securepoll-vote-id' ).value;
 67+ var strikeButton = document.getElementById( 'securepoll-strike-button' );
 68+ var unstrikeButton = document.getElementById( 'securepoll-unstrike-button' );
 69+ var spinner = document.getElementById( 'securepoll-strike-spinner' );
 70+ strikeButton.disabled = true;
 71+ unstrikeButton.disabled = true;
 72+ spinner.style.display = 'block';
 73+ var reason = document.getElementById( 'securepoll-strike-reason' ).value;
 74+
 75+ var processResult = function (xhr) {
 76+ spinner.style.display = 'none';
 77+ strikeButton.disabled = false;
 78+ unstrikeButton.disabled = false;
 79+
 80+ if ( xhr.status >= 300 || xhr.status < 200 ) {
 81+ document.getElementById( 'securepoll-strike-result' ).innerHTML = xhr.responseText;
 82+ return;
 83+ }
 84+
 85+ // Evaluate JSON result, with brackets to avoid interpretation as a code block
 86+ result = eval( '(' + xhr.responseText + ')' );
 87+ if ( result.status == 'good' ) {
 88+ popup.style.display = 'none';
 89+ } else {
 90+ document.getElementById( 'securepoll-strike-result' ).innerHTML = result.message;
 91+ }
 92+
 93+ securepoll_modify_document( action, id );
 94+ };
 95+
 96+ sajax_do_call( 'wfSecurePollStrike', [ action, id, reason ], processResult );
 97+}
 98+
 99+function securepoll_modify_document( action, voteId ) {
 100+ var popupButton = document.getElementById( 'securepoll-popup-' + voteId );
 101+ var row = popupButton.parentNode.parentNode;
 102+ if ( action == 'strike' ) {
 103+ row.className += ' securepoll-struck-vote';
 104+ popupButton.value = securepoll_unstrike_button;
 105+ } else {
 106+ row.className = row.className.replace( 'securepoll-struck-vote', '' );
 107+ popupButton.value = securepoll_strike_button;
 108+ }
 109+ popupButton.onclick = function (event) {
 110+ securepoll_strike_popup( event, action == 'strike' ? 'unstrike' : 'strike', voteId );
 111+ }
 112+}
Property changes on: trunk/extensions/SecurePoll/resources/SecurePoll.js
___________________________________________________________________
Added: svn:mergeinfo
Added: svn:eol-style
1113 + native
Index: trunk/extensions/SecurePoll/SecurePoll.sql
@@ -1,106 +1,216 @@
22
 3+-- Generic entity ID allocation
34 CREATE TABLE /*_*/securepoll_entity (
 5+ -- ID
46 en_id int not null primary key auto_increment,
 7+
 8+ -- "election", "question" or "option"
59 en_type varbinary(32) not null
610 ) /*$wgDBTableOptions*/;
711
 12+
 13+-- i18n text associated with an entity
814 CREATE TABLE /*_*/securepoll_msgs (
 15+ -- securepoll_entity.en_id
916 msg_entity int not null,
 17+
 18+ -- Language code
1019 msg_lang varbinary(32) not null,
 20+
 21+ -- Message key
1122 msg_key varbinary(32) not null,
 23+
 24+ -- Message text, UTF-8 encoded
1225 msg_text mediumtext not null
1326 ) /*$wgDBTableOptions*/;
1427 CREATE UNIQUE INDEX /*i*/spmsg_entity ON /*_*/securepoll_msgs (msg_entity, msg_lang, msg_key);
1528
 29+
 30+-- key/value pairs (properties) associated with an entity
1631 CREATE TABLE /*_*/securepoll_properties (
 32+ -- securepoll_entity.en_id
1733 pr_entity int not null,
 34+
 35+ -- Property key
1836 pr_key varbinary(32) not null,
 37+
 38+ -- Property value
1939 pr_value mediumblob not null
2040 ) /*$wgDBTableOptions*/;
2141 CREATE UNIQUE INDEX /*i*/sppr_entity ON /*_*/securepoll_properties (pr_entity, pr_key);
2242
 43+
 44+-- List of elections (or polls, surveys, etc)
2345 CREATE TABLE /*_*/securepoll_elections (
 46+ -- securepoll_entity.en_id
2447 el_entity int not null primary key,
 48+
 49+ -- Election title
 50+ -- Only used for the election list on the entry page
2551 el_title varchar(255) not null,
 52+
 53+ -- Owner user.user_id
 54+ el_owner int not null,
 55+
 56+ -- Ballot type, see Ballot.php
2657 el_ballot varchar(32) not null,
 58+
 59+ -- Tally type, see Tally.php
2760 el_tally varchar(32) not null,
 61+
 62+ -- Primary (administrative) language
 63+ -- This is the primary source for translations
2864 el_primary_lang varbinary(32) not null,
 65+
 66+ -- Start date, in 14-char MW format
2967 el_start_date varbinary(14),
 68+
 69+ -- End date, in 14-char MW format
3070 el_end_date varbinary(14),
 71+
 72+ -- User authorisation type, see Auth.php
3173 el_auth_type varbinary(32) not null
3274 ) /*$wgDBTableOptions*/;
3375 CREATE UNIQUE INDEX /*i*/spel_title ON /*_*/securepoll_elections (el_title);
3476
 77+
 78+-- Questions, see Question.php
3579 CREATE TABLE /*_*/securepoll_questions (
 80+ -- securepoll_entity.en_id
3681 qu_entity int not null primary key,
 82+
 83+ -- securepoll_elections.el_entity
3784 qu_election int not null,
 85+
 86+ -- Index determining the order the questions are shown, if shuffle is off
3887 qu_index int not null
3988 ) /*$wgDBTableOptions*/;
4089 CREATE INDEX /*i*/spqu_election_index ON /*_*/securepoll_questions (qu_election, qu_index, qu_entity);
4190
 91+
 92+-- Options for answering a given question, see Option.php
4293 CREATE TABLE /*_*/securepoll_options (
 94+ -- securepoll_entity.en_id
4395 op_entity int not null primary key,
 96+ -- securepoll_elections.el_entity
4497 op_election int not null,
 98+ -- securepoll_questions.qu_entity
4599 op_question int not null
46100 ) /*$wgDBTableOptions*/;
47101 CREATE INDEX /*i*/spop_question ON /*_*/securepoll_options (op_question, op_entity);
48102
 103+-- Voter list, independent for each election
 104+-- See Voter.php
49105 CREATE TABLE /*_*/securepoll_voters (
 106+ -- Primary key
50107 voter_id int not null primary key auto_increment,
 108+ -- securepoll_elections.el_id
51109 voter_election int not null,
 110+ -- The voter's name, as it appears on the remote site
52111 voter_name varchar(255) binary not null,
 112+ -- The auth type that created this voter
53113 voter_type varbinary(32) not null,
 114+ -- The voter's domain, should be fully-qualified
54115 voter_domain varbinary(255) not null,
 116+ -- A URL uniquely identifying the voter
55117 voter_url blob,
 118+ -- serialized properties blob
56119 voter_properties blob
57120 ) /*$wgDBTableOptions*/;
58121 CREATE INDEX /*i*/spvoter_elec_name_domain ON /*_*/securepoll_voters
59122 (voter_election, voter_name, voter_domain);
60123
 124+-- Votes that have been cast
 125+-- Contains a blob with answers to all questions
61126 CREATE TABLE /*_*/securepoll_votes (
62127 vote_id int not null primary key auto_increment,
 128+ -- securepoll_elections.el_id
63129 vote_election int not null,
 130+ -- securepoll_voters.voter_id
64131 vote_voter int not null,
65132
66133 -- Denormalised fields from the user table for efficient sorting
 134+
 135+ -- securepoll_voters.voter_name
67136 vote_voter_name varchar(255) binary not null,
 137+ -- securepoll_voters.voter_domain
68138 vote_voter_domain varbinary(32) not null,
69139
70140 -- Denormalised field from the strike table
71141 -- 1 if struck, 0 if not struck
72142 vote_struck tinyint not null,
73143
 144+ -- The voting record, produced and interpreted by the ballot type
 145+ -- May be encrypted
74146 vote_record blob not null,
 147+
 148+ -- The IP address, in hexadecimal form (IP::toHex())
75149 vote_ip varbinary(32) not null,
 150+
 151+ -- The X-Forwarded-For header
76152 vote_xff varbinary(255) not null,
 153+
 154+ -- The User-Agent header
77155 vote_ua varbinary(255) not null,
 156+
 157+ -- MW-format timestamp when the vote was cast
78158 vote_timestamp varbinary(14) not null,
 159+
 160+ -- 1 if the vote is current, 0 if old
 161+ -- Only one vote with a given voter will have vote_current=1
79162 vote_current tinyint not null,
 163+
 164+ -- 1 if the CSRF token matched (good), 0 for a potential hack
80165 vote_token_match tinyint not null,
 166+
 167+ -- 1 if the vote is flagged as being made by a potential sockpuppet
 168+ -- Details in securepoll_cookie_match
81169 vote_cookie_dup tinyint not null
82170 ) /*$wgDBTableOptions*/;
 171+-- For list subpage, sorted by timestamp
83172 CREATE INDEX /*i*/spvote_timestamp ON /*_*/securepoll_votes
84173 (vote_election, vote_timestamp);
 174+-- For list subpage, sorted by name
85175 CREATE INDEX /*i*/spvote_voter_name ON /*_*/securepoll_votes
86176 (vote_election, vote_voter_name, vote_timestamp);
 177+-- For list subpage, sorted by domain
87178 CREATE INDEX /*i*/spvote_voter_domain ON /*_*/securepoll_votes
88179 (vote_election, vote_voter_domain, vote_timestamp);
 180+-- For list subpage, sorted by IP
89181 CREATE INDEX /*i*/spvote_ip ON /*_*/securepoll_votes
90182 (vote_election, vote_ip, vote_timestamp);
91183
 184+-- Log of admin strike actions
92185 CREATE TABLE /*_*/securepoll_strike (
 186+ -- Primary key
93187 st_id int not null primary key auto_increment,
 188+
 189+ -- securepoll_votes.vote_id
94190 st_vote int not null,
 191+
 192+ -- Time at which the action occurred
95193 st_timestamp varbinary(14) not null,
 194+
 195+ -- "strike" or "unstrike"
96196 st_action varbinary(32) not null,
 197+
 198+ -- Explanatory reason
97199 st_reason varchar(255) not null,
 200+
 201+ -- user.user_id who did the action
98202 st_user int not null
99203 ) /*$wgDBTableOptions*/;
 204+-- For details subpage (strike log)
100205 CREATE INDEX /*i*/spstrike_vote ON /*_*/securepoll_strike
101206 (st_vote, st_timestamp);
102207
 208+
 209+-- Local voter qualification lists
 210+-- Currently manually populated, referenced by Auth.php
103211 CREATE TABLE /*_*/securepoll_lists (
 212+ -- List name
104213 li_name varbinary(255),
 214+ -- user.user_id
105215 li_member int not null
106216 ) /*$wgDBTableOptions*/;
107217 CREATE INDEX /*i*/splists_name ON /*_*/securepoll_lists
@@ -108,11 +218,17 @@
109219 CREATE INDEX /*i*/splists_member ON /*_*/securepoll_lists
110220 (li_member, li_name);
111221
 222+-- Suspicious cookie match logs
112223 CREATE TABLE /*_*/securepoll_cookie_match (
 224+ -- Primary key
113225 cm_id int not null primary key auto_increment,
 226+ -- securepoll_elections.el_id
114227 cm_election int not null,
 228+ -- securepoll_voters.voter_id
115229 cm_voter_1 int not null,
 230+ -- securepoll_voters.voter_id
116231 cm_voter_2 int not null,
 232+ -- Timestamp at which the match was logged
117233 cm_timestamp varbinary(14) not null
118234 ) /*$wgDBTableOptions*/;
119235 CREATE INDEX /*i*/spcookie_match_voter_1 ON /*_*/securepoll_cookie_match
Index: trunk/extensions/SecurePoll/SecurePoll.i18n.php
@@ -57,10 +57,11 @@
5858 'securepoll-no-decryption-key' => 'No decryption key is configured.
5959 Cannot decrypt.',
6060 'securepoll-jump' => 'Go to the voting server',
61 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
62 - Your vote was invalid: $1
63 - </div>',
 61+ 'securepoll-bad-ballot-submission' => 'Your vote was invalid: $1',
6462 'securepoll-unanswered-questions' => 'You must answer all questions.',
 63+ 'securepoll-invalid-rank' => 'Invalid rank. You must give candidates a rank between 1 and 999.',
 64+ 'securepoll-unranked-options' => 'Some options were not ranked.
 65+You must give all options a rank between 1 and 999.',
6566
6667 # Authorisation related
6768 'securepoll-remote-auth-error' => 'Error fetching your account information from the server.',
@@ -94,6 +95,7 @@
9596 'securepoll-strike-reason' => 'Reason:',
9697 'securepoll-strike-cancel' => 'Cancel',
9798 'securepoll-strike-error' => 'Error performing strike/unstrike: $1',
 99+ 'securepoll-strike-token-mismatch' => 'Session data lost',
98100 'securepoll-details-link' => 'Details',
99101
100102 # Details page
@@ -116,6 +118,8 @@
117119 'securepoll-dump-not-finished' => 'Encrypted election records are only available after the finish date on $1 at $2',
118120 'securepoll-dump-no-urandom' => 'Cannot open /dev/urandom.
119121 To maintain voter privacy, encrypted election records are only publically available when they can be shuffled with a secure random number stream.',
 122+ 'securepoll-urandom-not-supported' => 'This server does not support cryptographic random number generation.
 123+To maintain voter privacy, encrypted election records are only publically available when they can be shuffled with a secure random number stream.',
120124
121125 # Translate page
122126 'securepoll-translate-title' => 'Translate: $1',
@@ -251,9 +255,7 @@
252256 'securepoll-no-decryption-key' => 'لا توجد مفاتيح فك شفرة مهيئة.
253257 لا يمكن فك الشفرة.',
254258 'securepoll-jump' => 'اذهب إلى خادم التصويت',
255 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
256 -تصويتك ليس صحيحا: $1
257 -</div>',
 259+ 'securepoll-bad-ballot-submission' => 'تصويتك ليس صحيحا: $1',
258260 'securepoll-unanswered-questions' => 'يجب أن تجيب على كل الأسئلة.',
259261 'securepoll-remote-auth-error' => 'خطأ عند جلب معلومات حسابك من الخادوم.',
260262 'securepoll-remote-parse-error' => 'خطأ عند تفسير رد التصريح من الخادوم.',
@@ -844,9 +846,7 @@
845847 'securepoll-no-decryption-key' => "Nid yw'r allwedd dadgryptio wedi ei ffurfweddu.
846848 Ni ellir dadgryptio.",
847849 'securepoll-jump' => 'Mynd i weinydd y pleidleisio',
848 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
849 -Nid oedd eich pleidlais yn ddilys: $1
850 -</div>',
 850+ 'securepoll-bad-ballot-submission' => 'Nid oedd eich pleidlais yn ddilys: $1',
851851 'securepoll-unanswered-questions' => 'Rhaid ateb pob cwestiwn.',
852852 'securepoll-remote-auth-error' => "Cafwyd gwall wrth nôl gwybodaeth eich cyfrif o'r gweinydd.",
853853 'securepoll-remote-parse-error' => "Cafwyd gwall wrth ddehongli ymateb y gweinydd i'r cais awdurdodi.",
@@ -961,9 +961,7 @@
962962 'securepoll-no-decryption-key' => 'Ingen dekrypteringsnøgle opsat.
963963 Kan ikke dekryptere.',
964964 'securepoll-jump' => 'Gå til stemmeserveren',
965 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
966 -Din stemme var ugyldig: $1
967 -</div>',
 965+ 'securepoll-bad-ballot-submission' => 'Din stemme var ugyldig: $1',
968966 'securepoll-unanswered-questions' => 'Du skal besvare alle spørgsmålene.',
969967 'securepoll-remote-auth-error' => 'Der opstod en fejl under hentning af dine kontoinformationer fra serveren.',
970968 'securepoll-remote-parse-error' => 'Der opstod en fejl under læsning af autorisationssvarene fra serveren.',
@@ -1343,9 +1341,7 @@
13441342 'securepoll-no-decryption-key' => 'Δεν έχει ρυθμιστεί κλειδί αποκρυπτογράφησης.
13451343 Δεν είναι δυνατή η αποκρυπτογράφηση.',
13461344 'securepoll-jump' => 'Μετάβαση στον διακομιστή ψηφοφορίας',
1347 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
1348 -Η ψήφος σας ήταν άκυρη: $1
1349 -</div>',
 1345+ 'securepoll-bad-ballot-submission' => 'Η ψήφος σας ήταν άκυρη: $1',
13501346 'securepoll-unanswered-questions' => 'Πρέπει να απαντήσεις σε όλες τις ερωτήσεις.',
13511347 'securepoll-remote-auth-error' => 'Σφάλμα κατά την ανάκτηση των πληροφοριών για τον λογαριασμό σας από τον διακομιστή.',
13521348 'securepoll-remote-parse-error' => 'Σφάλμα στην ερμηνεία της απάντησης για άδεια πρόβασης από τον διακομιστή.',
@@ -1458,9 +1454,7 @@
14591455 'securepoll-no-decryption-key' => 'Neniu malĉifra ŝlosilo estas konfigurita.
14601456 Ne eblas malĉifri.',
14611457 'securepoll-jump' => 'Iri al la voĉdona servilo',
1462 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
1463 -Via voĉdono estis malvalida: $1
1464 -</div>',
 1458+ 'securepoll-bad-ballot-submission' => 'Via voĉdono estis malvalida: $1',
14651459 'securepoll-unanswered-questions' => 'Vi devas respondi al ĉiuj demandoj.',
14661460 'securepoll-remote-auth-error' => 'Eraro akirante vian kontinformon de la servilo.',
14671461 'securepoll-remote-parse-error' => 'Eraro interpretante la aŭtoritadan respondon de la servilo.',
@@ -1759,9 +1753,7 @@
17601754 'securepoll-no-decryption-key' => 'Salauksen purkuavainta ei ole asetettu.
17611755 Salausta ei voi purkaa.',
17621756 'securepoll-jump' => 'Siirry äänestyspalvelimelle.',
1763 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
1764 -Äänesi oli epäkelpo: $1
1765 -</div>',
 1757+ 'securepoll-bad-ballot-submission' => 'Äänesi oli epäkelpo: $1',
17661758 'securepoll-unanswered-questions' => 'Sinun täytyy vastata kaikkiin kysymyksiin.',
17671759 'securepoll-remote-auth-error' => 'Virhe hakiessa käyttäjätilisi tietoja palvelimelta.',
17681760 'securepoll-remote-parse-error' => 'Virhe tulkittaessa lupavastausta palvelimelta.',
@@ -2366,9 +2358,7 @@
23672359 'securepoll-no-decryption-key' => 'Dekripcijski ključ nije konfiguriran.
23682360 Dekripcija nije moguća.',
23692361 'securepoll-jump' => 'Idi na poslužitelj za glasovanje',
2370 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
2371 -Vaš glas je bio nevažeći: $1
2372 -</div>',
 2362+ 'securepoll-bad-ballot-submission' => 'Vaš glas je bio nevažeći: $1',
23732363 'securepoll-unanswered-questions' => 'Morate odgovoriti na sva pitanja.',
23742364 'securepoll-remote-auth-error' => 'Pogreška pri dobavljanje informacije o Vašem računu s poslužitelja.',
23752365 'securepoll-remote-parse-error' => 'Pogreška pri tumačenju autorizacijskog odgovora s poslužitelja.',
@@ -2597,9 +2587,7 @@
25982588 'securepoll-no-decryption-key' => 'Nincs visszafejtő kulcs beállítva.
25992589 Nem lehet visszafejteni.',
26002590 'securepoll-jump' => 'Irány a szavazás-szerverre',
2601 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
2602 -A szavazatod érvénytelen volt: $1
2603 -</div>',
 2591+ 'securepoll-bad-ballot-submission' => 'A szavazatod érvénytelen volt: $1',
26042592 'securepoll-unanswered-questions' => 'Minden kérdésre válaszolnod kell.',
26052593 'securepoll-remote-auth-error' => 'Nem sikerült lekérdezni a felhasználói fiókod adatait a szerverről.',
26062594 'securepoll-remote-parse-error' => 'Nem sikerült értelmezni a szerver autorizációs válaszát.',
@@ -2821,9 +2809,7 @@
28222810 'securepoll-no-decryption-key' => 'Kunci dekripsi belum dikonfigurasikan.
28232811 Tidak dapat melakukan dekripsi.',
28242812 'securepoll-jump' => 'Pergi ke server pemungutan suara',
2825 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
2826 -Suara Anda tidak valid: $1
2827 -</div>',
 2813+ 'securepoll-bad-ballot-submission' => 'Suara Anda tidak valid: $1',
28282814 'securepoll-unanswered-questions' => 'Anda harus menjawab semua pertanyaan.',
28292815 'securepoll-remote-auth-error' => 'Terjadi kesalahan ketika menarik informasi akun Anda dari server.',
28302816 'securepoll-remote-parse-error' => 'Terjadi kesalahan interpretasi atas respons otorisasi dari server.',
@@ -2949,9 +2935,7 @@
29502936 'securepoll-no-decryption-key' => 'Nessuna chiave di decrittazione è configurata.
29512937 Impossibile decifrare.',
29522938 'securepoll-jump' => 'Vai al server della votazione',
2953 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
2954 -Il tuo voto non era valido: $1
2955 -</div>',
 2939+ 'securepoll-bad-ballot-submission' => 'Il tuo voto non era valido: $1',
29562940 'securepoll-unanswered-questions' => 'È necessario rispondere a tutte le domande.',
29572941 'securepoll-remote-auth-error' => 'Errore durante il recupero delle informazioni sul tuo account dal server.',
29582942 'securepoll-remote-parse-error' => "Errore nell'interpretare la risposta di autorizzazione dal server.",
@@ -3165,9 +3149,7 @@
31663150 'securepoll-no-decryption-key' => '암호 해독 키가 설정되지 않았습니다.
31673151 암호를 해독할 수 없습니다.',
31683152 'securepoll-jump' => '선거 서버로 이동하기',
3169 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3170 -당신의 투표가 무효화되었습니다: $1
3171 -</div>',
 3153+ 'securepoll-bad-ballot-submission' => '당신의 투표가 무효화되었습니다: $1',
31723154 'securepoll-unanswered-questions' => '모든 질문에 답을 하셔야 합니다.',
31733155 'securepoll-remote-auth-error' => '귀하의 계정 정보를 불러오는 중에 오류가 발생하였습니다.',
31743156 'securepoll-remote-parse-error' => '서버로부터 권한 응답에 따른 해석 오류가 발생',
@@ -3388,9 +3370,7 @@
33893371 'securepoll-no-decryption-key' => 'Et ass keen Ëntschlësungsschlëssel agestallt.
33903372 Ëntschlësselung onméiglech.',
33913373 'securepoll-jump' => 'Op den Ofstëmmungs-Server goen',
3392 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3393 -Är Stëmm ass net valabel: $1
3394 -</div>',
 3374+ 'securepoll-bad-ballot-submission' => 'Är Stëmm ass net valabel: $1',
33953375 'securepoll-unanswered-questions' => 'Dir musst all Froe beäntwerten',
33963376 'securepoll-remote-auth-error' => 'Feeler beim Ofruf vun Äre Benotzerkontinformatioune vum Server.',
33973377 'securepoll-remote-parse-error' => 'Feeler beim Interpretéiere vun der Autorisatioun déi de Server geschéckt huet.',
@@ -3501,9 +3481,7 @@
35023482 'securepoll-no-decryption-key' => "d'r Is geine decryptiesjleutel ingesjteld.
35033483 Decodere is neet meugelik.",
35043484 'securepoll-jump' => 'Gank nao de sjtömserver',
3505 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3506 -Dien sjtöm is óngeldig: $1
3507 -</div>',
 3485+ 'securepoll-bad-ballot-submission' => 'Dien sjtöm is óngeldig: $1',
35083486 'securepoll-unanswered-questions' => 'Doe mós alle vraoge beantjwaorde.',
35093487 'securepoll-remote-auth-error' => "d'r Is 'n fout opgetraoje bie 't ophaole van dien gebroekersinformatie van de server.",
35103488 'securepoll-remote-parse-error' => "d'r Is 'n fout opgetraoje bie 't interpretere van 't antjwaord van de server.",
@@ -3608,9 +3586,7 @@
36093587 'securepoll-no-decryption-key' => 'Nėra sukonfigūruoto atkodavimo rakto.
36103588 Negalima iššifruoti.',
36113589 'securepoll-jump' => 'Eiti į balsavimo serverį',
3612 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3613 -Jūsų balsas netinkamas: $1
3614 -</div>',
 3590+ 'securepoll-bad-ballot-submission' => 'Jūsų balsas netinkamas: $1',
36153591 'securepoll-unanswered-questions' => 'Turite atsakyti į visus klausimus.',
36163592 'securepoll-remote-auth-error' => 'Įvyko klaida pristatant jūsų sąskaitos informaciją iš serverio.',
36173593 'securepoll-remote-parse-error' => 'Klaida interpretuojant leidimo atsakymą iš serverio.',
@@ -3734,9 +3710,7 @@
37353711 'securepoll-no-decryption-key' => 'Tiada kunci penyahsulitan dibentuk.
37363712 Tidak dapat menyahsulit.',
37373713 'securepoll-jump' => 'Pergi ke pelayan undian',
3738 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3739 -Undi anda tak sah: $1
3740 -</div>',
 3714+ 'securepoll-bad-ballot-submission' => 'Undi anda tak sah: $1',
37413715 'securepoll-unanswered-questions' => 'Anda perlu jawab kesemua soalan.',
37423716 'securepoll-remote-auth-error' => 'Ralat dalam mengambil maklumat akaun anda dari pelayan.',
37433717 'securepoll-remote-parse-error' => 'Ralat menafsirkan jawapan kebenaran dari pelayan.',
@@ -3849,9 +3823,7 @@
38503824 'securepoll-no-decryption-key' => "L-ebda ċavetta ta' dekritazzjoni ma ġiet konfigurata.
38513825 Impossibbli li tiġi deċifrata.",
38523826 'securepoll-jump' => 'Mur fis-server tal-votazzjoni',
3853 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3854 -Il-vot tiegħek kien invalidu: $1
3855 -</div>',
 3827+ 'securepoll-bad-ballot-submission' => 'Il-vot tiegħek kien invalidu: $1',
38563828 'securepoll-unanswered-questions' => 'Trid tirrispondi kull mistoqsija.',
38573829 'securepoll-remote-auth-error' => 'Żball waqt ir-ripristinaġġ mis-server tal-informazzjoni fuq il-kont tiegħek.',
38583830 'securepoll-remote-parse-error' => "Żball fl-interpretazzjoni mis-server tar-risposta ta' awtorizzazzjoni.",
@@ -3964,9 +3936,7 @@
39653937 'securepoll-no-decryption-key' => 'Keen Opslötel-Slötel instellt.
39663938 Opslöteln geiht nich.',
39673939 'securepoll-jump' => 'Na’n Afstimmserver gahn',
3968 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
3969 -Dien Stimm weer ungüllig: $1
3970 -</div>',
 3940+ 'securepoll-bad-ballot-submission' => 'Dien Stimm weer ungüllig: $1',
39713941 'securepoll-unanswered-questions' => 'Du musst all Fragen antern.',
39723942 'securepoll-remote-auth-error' => 'Fehler bi’t Afropen vun dien Brukerkonteninfos vun’n Server.',
39733943 'securepoll-remote-parse-error' => 'Fehler bi’t Interpreteren vun de Antwoord vun’n Server to de Rechten.',
@@ -4208,9 +4178,7 @@
42094179 'securepoll-no-decryption-key' => 'Ingen dekrypteringsnøkkel er sett opp.
42104180 Kan ikkje dekryptera.',
42114181 'securepoll-jump' => 'Gå til stemmetenaren',
4212 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4213 -Di stemme var ugyldig: $1
4214 -</div>',
 4182+ 'securepoll-bad-ballot-submission' => 'Di stemme var ugyldig: $1',
42154183 'securepoll-unanswered-questions' => 'Du må svara på alle spørsmåla.',
42164184 'securepoll-remote-auth-error' => 'Feil oppstod ved henting av kontoinformasjonen din frå filtenaren.',
42174185 'securepoll-remote-parse-error' => 'Feil oppsto i samband med tolking av autorisasjonssvar frå tenaren',
@@ -4319,9 +4287,7 @@
43204288 'securepoll-no-decryption-key' => 'Ingen dekrypteringsnøkkel er konfigurert.
43214289 Kan ikke dekryptere.',
43224290 'securepoll-jump' => 'Gå til stemmetjeneren',
4323 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4324 -Din stemme var ugyldig: $1
4325 -</div>',
 4291+ 'securepoll-bad-ballot-submission' => 'Din stemme var ugyldig: $1',
43264292 'securepoll-unanswered-questions' => 'Du må besvare alle spørsmålene.',
43274293 'securepoll-remote-auth-error' => 'Feil oppsto ved henting av din kontoinformasjon fra tjeneren.',
43284294 'securepoll-remote-parse-error' => 'Feil oppsto ved tolkning av autorisasjonssvar fra tjeneren.',
@@ -4547,9 +4513,7 @@
45484514 'securepoll-no-decryption-key' => 'No tin un klave deskriptivó konfigurá.
45494515 No por decrypt.',
45504516 'securepoll-jump' => 'Bai na e server di votashon',
4551 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4552 -Bo voto no ta balido: $1
4553 -</div>',
 4517+ 'securepoll-bad-ballot-submission' => 'Bo voto no ta balido: $1',
45544518 'securepoll-unanswered-questions' => 'Bo mester kontestá tur e preguntanan.',
45554519 'securepoll-remote-auth-error' => 'Tabatin problema na ora di buska informashonnan tokante di bo kuenta riba e server.',
45564520 'securepoll-remote-parse-error' => 'Tabatin problema na ora di interpretá e derechinan for di riba e server.',
@@ -4678,9 +4642,7 @@
46794643 'securepoll-no-decryption-key' => 'Klucz odszyfrowujący nie został skonfigurowany.
46804644 Odszyfrowanie nie jest możliwe.',
46814645 'securepoll-jump' => 'Przejdź do serwera obsługującego głosowanie',
4682 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4683 -Twój głos był nieważny – $1
4684 -</div>',
 4646+ 'securepoll-bad-ballot-submission' => 'Twój głos był nieważny – $1',
46854647 'securepoll-unanswered-questions' => 'Musisz odpowiedzieć na wszystkie pytania.',
46864648 'securepoll-remote-auth-error' => 'Wystąpił błąd podczas pobierania informacji z serwera o Twoim koncie.',
46874649 'securepoll-remote-parse-error' => 'Wystąpił błąd interpretacji odpowiedzi autoryzującej z serwera.',
@@ -4793,9 +4755,7 @@
47944756 'securepoll-no-decryption-key' => 'Pa gnun-e ciav ëd decifrassion a son configurà.
47954757 As peul pa decifré.',
47964758 'securepoll-jump' => 'Va al server ëd la votassion',
4797 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4798 -Tò vot a l\'era pa vàlid: $1
4799 -</div>',
 4759+ 'securepoll-bad-ballot-submission' => 'Tò vot a l\'era pa vàlid: $1',
48004760 'securepoll-unanswered-questions' => 'It deuve arsponde a tute le custion.',
48014761 'securepoll-remote-auth-error' => 'Eror an lesend le anformassion ëd tò cont dal server.',
48024762 'securepoll-remote-parse-error' => "Eror an antërpretand l'arspòsta d'autorisassion dal server.",
@@ -4910,9 +4870,7 @@
49114871 'securepoll-no-decryption-key' => 'Nenhuma chave de descodificação está configurada.
49124872 Não é possível descodificar.',
49134873 'securepoll-jump' => 'Ir para o servidor de votação',
4914 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
4915 -O seu voto foi inválido: $1
4916 -</div>',
 4874+ 'securepoll-bad-ballot-submission' => 'O seu voto foi inválido: $1',
49174875 'securepoll-unanswered-questions' => 'Você deve responder todas as perguntas.',
49184876 'securepoll-remote-auth-error' => 'Erro ao buscar as informações da sua conta a partir do servidor.',
49194877 'securepoll-remote-parse-error' => 'Erro ao interpretar a resposta de autorização do servidor.',
@@ -5027,9 +4985,7 @@
50284986 'securepoll-no-decryption-key' => 'Nenhuma chave de descriptografia está configurada.
50294987 Não foi possível descriptografar.',
50304988 'securepoll-jump' => 'Ir para o servidor de votação',
5031 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
5032 -Seu voto foi inválido: $1
5033 -</div>',
 4989+ 'securepoll-bad-ballot-submission' => 'Seu voto foi inválido: $1',
50344990 'securepoll-unanswered-questions' => 'Você deve responder todas as questões.',
50354991 'securepoll-remote-auth-error' => 'Erro ao tentar obter suas informações de conta do servidor.',
50364992 'securepoll-remote-parse-error' => 'Erro ao interpretar a resposta de autorização do servidor.',
@@ -5299,9 +5255,7 @@
53005256 'securepoll-no-decryption-key' => 'Расшифровка күлүүһэ настройкаламматах.
53015257 Расшифровкалыыр табыллыбат.',
53025258 'securepoll-jump' => 'Куоластааһын сиэрбэригэр көһүү',
5303 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
5304 -Эн куолаһыҥ ааҕыллыбат: $1
5305 -</div>',
 5259+ 'securepoll-bad-ballot-submission' => 'Эн куолаһыҥ ааҕыллыбат: $1',
53065260 'securepoll-unanswered-questions' => 'Бары ыйытыыларга хоруйдуохтааххын.',
53075261 'securepoll-remote-auth-error' => 'Аат-суол туһунан сибидиэнньэлэри сиэрбэртэн ылыыга алҕас таҕыста.',
53085262 'securepoll-remote-parse-error' => 'Сиэрбэртэн авторизацияны сыыһа көрүү буолбутун туһунан хоруй кэллэ.',
@@ -5545,9 +5499,7 @@
55465500 'securepoll-no-decryption-key' => 'Ingen dekrypteringsnyckel är konfigurerad.
55475501 Kan inte dekryptera.',
55485502 'securepoll-jump' => 'Gå till röstnings-servern.',
5549 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
5550 -Din röst var ogiltig: $1
5551 -</div>',
 5503+ 'securepoll-bad-ballot-submission' => 'Din röst var ogiltig: $1',
55525504 'securepoll-unanswered-questions' => 'Du måste svara på alla frågor.',
55535505 'securepoll-remote-auth-error' => 'Fel uppstod vid hämtning av din kontoinformation från servern.',
55545506 'securepoll-remote-parse-error' => 'Fel uppstod vid tolkning av auktorisationssvar från servern.',
@@ -5740,9 +5692,7 @@
57415693 'securepoll-no-decryption-key' => 'Walang nakaayos na susing pangtanggal ng kodigo.
57425694 Hindi matanggal ang kodigo.',
57435695 'securepoll-jump' => 'Pumunta sa tagapaghain ng pagboto',
5744 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
5745 -Hindi tinanggap ang boto mo: $1
5746 -</div>',
 5696+ 'securepoll-bad-ballot-submission' => 'Hindi tinanggap ang boto mo: $1',
57475697 'securepoll-unanswered-questions' => 'Dapat mong sagutin ang lahat ng mga katanungan.',
57485698 'securepoll-remote-auth-error' => 'Kamalian sa pagpulot ng kabatiran ng akawnt mo mula sa tagapaghain.',
57495699 'securepoll-remote-parse-error' => 'Kamalian sa pagpapaliwanag ng tugon ng pagpapahintulot mula sa tagapaghain.',
@@ -5968,9 +5918,7 @@
59695919 'securepoll-no-decryption-key' => 'Не налаштований ключ розшифрування.
59705920 Не в змозі розшифрувати.',
59715921 'securepoll-jump' => 'Перейти на сервер голосувань',
5972 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
5973 -Ваш голос не дійсний: $1
5974 -</div>',
 5922+ 'securepoll-bad-ballot-submission' => 'Ваш голос не дійсний: $1',
59755923 'securepoll-unanswered-questions' => 'Ви повинні відповісти на всі запитання.',
59765924 'securepoll-remote-auth-error' => 'Помилка отримання інформації з сервера про ваш обліковий запис.',
59775925 'securepoll-remote-parse-error' => 'Помилка інтерпретації відповіді від авторизації з сервера.',
@@ -6071,9 +6019,7 @@
60726020 'securepoll-no-decryption-key' => 'No xe stà configurà nissuna ciave de decritassion.
60736021 No se pole decritar.',
60746022 'securepoll-jump' => 'Và al server de ła votasion',
6075 - 'securepoll-bad-ballot-submission' => '<div class="securepoll-error-box">
6076 -El to voto no\'l xe mia vàłido: $1
6077 -</div>',
 6023+ 'securepoll-bad-ballot-submission' => 'El to voto no\'l xe mia vàłido: $1',
60786024 'securepoll-unanswered-questions' => 'Ti gà da rispóndar a tute le domande.',
60796025 'securepoll-remote-auth-error' => 'Eròr durante el recupero de le informassion su la to utensa dal server.',
60806026 'securepoll-remote-parse-error' => 'Se gà verificà un eròr interpretando la risposta de autorixassion dal server.',
Index: trunk/extensions/SecurePoll/SecurePoll.php
@@ -1,11 +1,8 @@
22 <?php
33 /**
4 - * Wikimedia Foundation Board of Trustees Election
5 - *
64 * @file
75 * @ingroup Extensions
86 * @author Tim Starling <tstarling@wikimedia.org>
9 - * @author Kwan Ting Chan
107 * @link http://www.mediawiki.org/wiki/Extension:SecurePoll Documentation
118 */
129
@@ -18,7 +15,7 @@
1916 $wgExtensionCredits['other'][] = array(
2017 'path' => __FILE__,
2118 'name' => 'SecurePoll',
22 - 'author' => array( 'Tim Starling', 'Kwan Ting Chan', 'others' ),
 19+ 'author' => array( 'Tim Starling', 'others' ),
2320 'url' => 'http://www.mediawiki.org/wiki/Extension:SecurePoll',
2421 'svn-date' => '$LastChangedDate$',
2522 'svn-revision' => '$LastChangedRevision$',
@@ -50,14 +47,15 @@
5148 $wgExtensionMessagesFiles['SecurePoll'] = "$dir/SecurePoll.i18n.php";
5249 $wgExtensionAliasesFiles['SecurePoll'] = "$dir/SecurePoll.alias.php";
5350
54 -$wgAutoloadClasses['SecurePollPage'] = "$dir/SecurePoll_body.php";
55 -$wgSpecialPages['SecurePoll'] = 'SecurePollPage';
 51+$wgSpecialPages['SecurePoll'] = 'SecurePoll_BasePage';
5652
5753 $wgAutoloadClasses = $wgAutoloadClasses + array(
 54+ 'SecurePoll' => "$dir/includes/Base.php",
5855 'SecurePoll_Auth' => "$dir/includes/Auth.php",
5956 'SecurePoll_LocalAuth' => "$dir/includes/Auth.php",
6057 'SecurePoll_RemoteMWAuth' => "$dir/includes/Auth.php",
6158 'SecurePoll_Ballot' => "$dir/includes/Ballot.php",
 59+ 'SecurePoll_BasePage' => "$dir/includes/Base.php",
6260 'SecurePoll_ChooseBallot' => "$dir/includes/Ballot.php",
6361 'SecurePoll_PreferentialBallot' => "$dir/includes/Ballot.php",
6462 'SecurePoll_Crypt' => "$dir/includes/Crypt.php",
@@ -73,6 +71,7 @@
7472 'SecurePoll_Option' => "$dir/includes/Option.php",
7573 'SecurePoll_Page' => "$dir/includes/Page.php",
7674 'SecurePoll_Question' => "$dir/includes/Question.php",
 75+ 'SecurePoll_Random' => "$dir/includes/Random.php",
7776 'SecurePoll_Tallier' => "$dir/includes/Tallier.php",
7877 'SecurePoll_PluralityTallier' => "$dir/includes/Tallier.php",
7978 'SecurePoll_TallyPage' => "$dir/includes/TallyPage.php",
Index: trunk/extensions/SecurePoll/includes/Voter.php
@@ -158,7 +158,7 @@
159159 }
160160 }
161161 } else {
162 - setcookie( $cookieName, $this->getId(), time() + 86400*3 );
 162+ setcookie( $cookieName, $this->getId(), time() + 86400*30 );
163163 }
164164 }
165165
Index: trunk/extensions/SecurePoll/includes/DumpPage.php
@@ -4,6 +4,8 @@
55 * Special:SecurePoll subpage for exporting encrypted election records.
66 */
77 class SecurePoll_DumpPage extends SecurePoll_Page {
 8+ var $headersSent;
 9+
810 /**
911 * Execute the subpage.
1012 * @param $params array Array of subpage parameters.
@@ -17,7 +19,7 @@
1820 }
1921
2022 $electionId = intval( $params[0] );
21 - $this->election = $this->parent->getElection( $electionId );
 23+ $this->election = SecurePoll::getElection( $electionId );
2224 if ( !$this->election ) {
2325 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2426 return;
@@ -40,89 +42,36 @@
4143 return;
4244 }
4345
44 - if ( !$this->openRandom() ) {
45 - $wgOut->addWikiMsg( 'securepoll-dump-no-urandom' );
 46+ $this->headersSent = false;
 47+ $status = $this->election->dumpVotesToCallback( array( $this, 'dumpVote' ) );
 48+ if ( !$status->isOK() && !$this->headersSent ) {
 49+ $wgOut->addWikiText( $status->getWikiText() );
4650 return;
4751 }
48 -
49 - $wgOut->disable();
50 - header( 'Content-Type: text/plain' );
51 - $filename = urlencode( "SecurePoll-$electionId-" . wfTimestampNow() );
52 - header( "Content-Disposition: attachment; filename=$filename" );
53 - $db = wfGetDB( DB_SLAVE );
54 - $res = $db->select(
55 - 'securepoll_votes',
56 - array( 'vote_id', 'vote_record' ),
57 - array(
58 - 'vote_election' => $electionId,
59 - 'vote_current' => 1,
60 - 'vote_struck' => 0
61 - ),
62 - __METHOD__
63 - );
64 -
65 - $order = $this->shuffle( range( 0, $res->numRows() - 1 ) );
66 - foreach ( $order as $i ) {
67 - $res->seek( $i );
68 - echo $res->fetchObject()->vote_record . "\n\n";
 52+ if ( !$this->headersSent ) {
 53+ $this->sendHeaders();
6954 }
70 - $this->closeRandom();
 55+ echo "</election>\n</SecurePoll>\n";
7156 }
7257
73 - /**
74 - * Open the /dev/urandom device
75 - * @return bool success
76 - */
77 - function openRandom() {
78 - if ( wfIsWindows() ) {
79 - return false;
 58+ function dumpVote( $election, $row ) {
 59+ if ( !$this->headersSent ) {
 60+ $this->sendHeaders();
8061 }
81 - $this->urandom = fopen( '/dev/urandom', 'rb' );
82 - if ( !$this->urandom ) {
83 - return false;
84 - }
85 - return true;
 62+ echo "<vote>" . $row->vote_record . "</vote>\n";
8663 }
8764
88 - /**
89 - * Close the urandom device
90 - */
91 - function closeRandom() {
92 - fclose( $this->urandom );
93 - }
 65+ function sendHeaders() {
 66+ global $wgOut;
9467
95 - /**
96 - * Get a random integer between 0 and ($maxp1 - 1)
97 - */
98 - function random( $maxp1 ) {
99 - $numBytes = ceil( strlen( base_convert( $maxp1, 10, 16 ) ) / 2 );
100 - if ( $numBytes == 0 ) {
101 - return 0;
102 - }
103 - $data = fread( $this->urandom, $numBytes );
104 - if ( strlen( $data ) != $numBytes ) {
105 - throw new MWException( __METHOD__.': not enough bytes' );
106 - }
107 - $x = 0;
108 - for ( $i = 0; $i < $numBytes; $i++ ) {
109 - $x *= 256;
110 - $x += ord( substr( $data, $i, 1 ) );
111 - }
112 - return $x % $maxp1;
 68+ $this->headersSent = true;
 69+ $wgOut->disable();
 70+ header( 'Content-Type: text/plain' );
 71+ $electionId = $this->election->getId();
 72+ $filename = urlencode( "SecurePoll-$electionId-" . wfTimestampNow() );
 73+ header( "Content-Disposition: attachment; filename=$filename" );
 74+ echo "<SecurePoll>\n<election>\n" .
 75+ $this->election->getConfXml();
 76+ SecurePoll_Entity::setLanguages( array( $this->election->getLanguage() ) );
11377 }
114 -
115 - /**
116 - * Works like shuffle() except more secure. Returns the new array instead
117 - * of modifying it. The algorithm is the Knuth/Durstenfeld kind.
118 - */
119 - function shuffle( $a ) {
120 - $a = array_values( $a );
121 - for ( $i = count( $a ) - 1; $i >= 0; $i-- ) {
122 - $target = $this->random( $i + 1 );
123 - $tmp = $a[$i];
124 - $a[$i] = $a[$target];
125 - $a[$target] = $tmp;
126 - }
127 - return $a;
128 - }
12978 }
Index: trunk/extensions/SecurePoll/includes/Election.php
@@ -56,7 +56,10 @@
5757 var $title, $ballotType, $tallyType, $primaryLang, $startDate, $endDate, $authType;
5858
5959 /**
60 - * Constructor.
 60+ * Constructor.
 61+ *
 62+ * Do not use this constructor directly, instead use SecurePoll::getElection().
 63+ *
6164 * @param $id integer
6265 */
6366 function __construct( $id ) {
@@ -293,16 +296,74 @@
294297 }
295298
296299 /**
297 - * Get the tallier object
 300+ * Get the tallier objects
298301 * @return SecurePoll_Tallier
299302 */
300 - function getTallier() {
301 - $tallier = SecurePoll_Tallier::factory( $this->tallyType, $this );
302 - if ( !$tallier ) {
303 - throw new MWException( 'Invalid tally type' );
 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;
304311 }
305 - return $tallier;
 312+ return $talliers;
306313 }
307314
 315+ /**
 316+ * Call a callback function for each valid vote record, in random order.
 317+ */
 318+ function dumpVotesToCallback( $callback ) {
 319+ if ( !$this->getCrypt() ) {
 320+ return Status::newFatal( 'securepoll-dump-no-crypt' );
 321+ }
 322+
 323+ $random = SecurePoll::getRandom();
 324+ $status = $random->open();
 325+ if ( !$status->isOK() ) {
 326+ return $status;
 327+ }
 328+ $db = wfGetDB( DB_SLAVE );
 329+ $res = $db->select(
 330+ 'securepoll_votes',
 331+ array( '*' ),
 332+ array(
 333+ 'vote_election' => $this->getId(),
 334+ 'vote_current' => 1,
 335+ 'vote_struck' => 0
 336+ ),
 337+ __METHOD__
 338+ );
 339+ if ( $res->numRows() ) {
 340+ $order = $random->shuffle( range( 0, $res->numRows() - 1 ) );
 341+ foreach ( $order as $i ) {
 342+ $res->seek( $i );
 343+ call_user_func( $callback, $this, $res->fetchObject() );
 344+ }
 345+ }
 346+ $random->close();
 347+ return Status::newGood();
 348+ }
 349+
 350+ /**
 351+ * Get an XML snippet describing the configuration of this object
 352+ */
 353+ function getConfXml() {
 354+ $s = "<configuration>\n" .
 355+ Xml::element( 'title', array(), $this->title ) . "\n" .
 356+ Xml::element( 'ballot', array(), $this->ballotType ) . "\n" .
 357+ Xml::element( 'tally', array(), $this->tallyType ) . "\n" .
 358+ Xml::element( 'lang', array(), $this->primaryLang ) . "\n" .
 359+ Xml::element( 'startDate', array(), wfTimestamp( TS_ISO_8601, $this->startDate ) ) . "\n" .
 360+ Xml::element( 'endDate', array(), wfTimestamp( TS_ISO_8601, $this->endDate ) ) . "\n" .
 361+ Xml::element( 'auth', array(), $this->authType ) . "\n" .
 362+ $this->getConfXmlEntityStuff();
 363+ foreach ( $this->getQuestions() as $question ) {
 364+ $s .= $question->getConfXml();
 365+ }
 366+ $s .= "</configuration>\n";
 367+ return $s;
 368+ }
308369 }
309370
Index: trunk/extensions/SecurePoll/includes/LoginPage.php
@@ -14,7 +14,7 @@
1515 }
1616
1717 $electionId = intval( $params[0] );
18 - $this->election = $this->parent->getElection( $electionId );
 18+ $this->election = SecurePoll::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 = $this->parent->getElection( $electionId );
 14+ $this->election = SecurePoll::getElection( $electionId );
1515 if ( !$this->election ) {
1616 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
1717 return;
@@ -18,7 +18,7 @@
1919
2020 $wgOut->disable();
2121 header( 'Content-Type: application/x-sql; charset=utf-8' );
22 - $filename = urlencode( "sp-msgs-$electionId-" . wfTimestampNow() );
 22+ $filename = urlencode( "sp-msgs-$electionId-" . wfTimestampNow() . '.sql' );
2323 header( "Content-Disposition: attachment; filename=$filename" );
2424 $dbr = wfGetDB( DB_SLAVE );
2525
Index: trunk/extensions/SecurePoll/includes/Question.php
@@ -31,5 +31,16 @@
3232 return $this->options;
3333 }
3434
35 - function getOptions() { return $this->options; }
 35+ function getOptions() {
 36+ return $this->options;
 37+ }
 38+
 39+ function getConfXml() {
 40+ $s = "<question>\n" . $this->getConfXmlEntityStuff();
 41+ foreach ( $this->getOptions() as $option ) {
 42+ $s .= $option->getConfXml();
 43+ }
 44+ $s .= "</question>\n";
 45+ return $s;
 46+ }
3647 }
Index: trunk/extensions/SecurePoll/includes/ListPage.php
@@ -20,7 +20,7 @@
2121 }
2222
2323 $electionId = intval( $params[0] );
24 - $this->election = $this->parent->getElection( $electionId );
 24+ $this->election = SecurePoll::getElection( $electionId );
2525 if ( !$this->election ) {
2626 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2727 return;
@@ -220,7 +220,7 @@
221221 function formatValue( $name, $value ) {
222222 global $wgLang;
223223 $critical = Xml::element( 'img',
224 - array( 'src' => $GLOBALS['wgStylePath'] . '/common/images/critical-32.png' )
 224+ array( 'src' => $GLOBALS['wgScriptPath'] . '/extensions/SecurePoll/resources/critical-32.png' )
225225 );
226226
227227 switch ( $name ) {
Index: trunk/extensions/SecurePoll/includes/Base.php
@@ -0,0 +1,104 @@
 2+<?php
 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+class SecurePoll_BasePage extends UnlistedSpecialPage {
 36+ static $pages = array(
 37+ 'details' => 'SecurePoll_DetailsPage',
 38+ 'dump' => 'SecurePoll_DumpPage',
 39+ 'entry' => 'SecurePoll_EntryPage',
 40+ 'list' => 'SecurePoll_ListPage',
 41+ 'login' => 'SecurePoll_LoginPage',
 42+ 'msgdump' => 'SecurePoll_MessageDumpPage',
 43+ 'tally' => 'SecurePoll_TallyPage',
 44+ 'translate' => 'SecurePoll_TranslatePage',
 45+ 'vote' => 'SecurePoll_VotePage',
 46+ );
 47+
 48+ /**
 49+ * Constructor
 50+ */
 51+ public function __construct() {
 52+ parent::__construct( 'SecurePoll' );
 53+ }
 54+
 55+ /**
 56+ * Show the special page
 57+ *
 58+ * @param $paramString Mixed: parameter passed to the page or null
 59+ */
 60+ public function execute( $paramString ) {
 61+ global $wgOut, $wgRequest, $wgScriptPath;
 62+
 63+ wfLoadExtensionMessages( 'SecurePoll' );
 64+
 65+ $this->setHeaders();
 66+ $wgOut->addLink( array(
 67+ 'rel' => 'stylesheet',
 68+ 'href' => "$wgScriptPath/extensions/SecurePoll/resources/SecurePoll.css",
 69+ 'type' => 'text/css'
 70+ ) );
 71+ $wgOut->addScriptFile( "$wgScriptPath/extensions/SecurePoll/resources/SecurePoll.js" );
 72+
 73+ $this->request = $wgRequest;
 74+
 75+ $paramString = strval( $paramString );
 76+ if ( $paramString === '' ) {
 77+ $paramString = 'entry';
 78+ }
 79+ $params = explode( '/', $paramString );
 80+ $pageName = array_shift( $params );
 81+ $page = $this->getSubpage( $pageName );
 82+ if ( !$page ) {
 83+ $wgOut->addWikiMsg( 'securepoll-invalid-page', $pageName );
 84+ return;
 85+ }
 86+
 87+ $page->execute( $params );
 88+ }
 89+
 90+ function getSubpage( $name ) {
 91+ if ( !isset( self::$pages[$name] ) ) {
 92+ return false;
 93+ }
 94+ $className = self::$pages[$name];
 95+ $page = new $className( $this );
 96+ return $page;
 97+ }
 98+
 99+ function getEditToken() {
 100+ if ( !isset( $_SESSION['spToken'] ) ) {
 101+ $_SESSION['spToken'] = sha1( mt_rand() . mt_rand() . mt_rand() );
 102+ }
 103+ return $_SESSION['spToken'];
 104+ }
 105+}
Property changes on: trunk/extensions/SecurePoll/includes/Base.php
___________________________________________________________________
Added: svn:mergeinfo
Added: svn:eol-style
1106 + native
Index: trunk/extensions/SecurePoll/includes/VotePage.php
@@ -19,7 +19,7 @@
2020 }
2121
2222 $electionId = intval( $params[0] );
23 - $this->election = $this->parent->getElection( $electionId );
 23+ $this->election = SecurePoll::getElection( $electionId );
2424 if ( !$this->election ) {
2525 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2626 return;
@@ -236,6 +236,7 @@
237237 throw new MWException( 'Configuration error: no jump-id' );
238238 }
239239 $url .= "/login/$id";
 240+ wfRunHooks( 'SecurePoll_JumpUrl', array( $this, &$url ) );
240241 $wgOut->addWikiText( $this->election->getMessage( 'jump-text' ) );
241242 $wgOut->addHTML(
242243 Xml::openElement( 'form', array( 'action' => $url, 'method' => 'post' ) ) .
Index: trunk/extensions/SecurePoll/includes/Entity.php
@@ -15,8 +15,9 @@
1616 var $messagesLoaded = array();
1717 var $properties;
1818
19 - static $languages = array();
 19+ static $languages = array( 'en' );
2020 static $messageCache = array();
 21+ static $parserOptions;
2122
2223 /**
2324 * Create an entity of the given type. This is typically called from the
@@ -182,6 +183,31 @@
183184 }
184185
185186 /**
 187+ * Get a message, and interpret it as wikitext, converting it to HTML.
 188+ */
 189+ function parseMessage( $name, $lineStart = true ) {
 190+ global $wgParser, $wgTitle;
 191+ if ( !self::$parserOptions ) {
 192+ self::$parserOptions = new ParserOptions;
 193+ }
 194+ if ( $wgTitle ) {
 195+ $title = $wgTitle;
 196+ } else {
 197+ $title = SpecialPage::getTitleFor( 'SecurePoll' );
 198+ }
 199+ $wikiText = $this->getMessage( $name );
 200+ $out = $wgParser->parse( $wikiText, $title, self::$parserOptions, $lineStart );
 201+ return $out->getText();
 202+ }
 203+
 204+ /**
 205+ * Get a message and convert it from wikitext to HTML, without <p> tags.
 206+ */
 207+ function parseMessageInline( $name ) {
 208+ return $this->parseMessage( $name, false );
 209+ }
 210+
 211+ /**
186212 * Get a property value. If it does not exist, the $default parameter
187213 * is passed back.
188214 * @param $name string
@@ -198,4 +224,40 @@
199225 }
200226 }
201227
 228+ /**
 229+ * Get all defined properties as an associative array
 230+ */
 231+ function getAllProperties() {
 232+ if ( $this->properties === null ) {
 233+ $this->loadProperties();
 234+ }
 235+ return $this->properties;
 236+ }
 237+
 238+ /**
 239+ * Get configuration XML. Overridden by most subclasses.
 240+ */
 241+ function getConfXml() {
 242+ return "<{$this->type}>\n" .
 243+ $this->getConfXmlEntityStuff() .
 244+ "</{$this->type}>\n";
 245+ }
 246+
 247+ /**
 248+ * Get an XML snippet giving the messages and properties
 249+ */
 250+ function getConfXmlEntityStuff() {
 251+ $s = Xml::element( 'id', array(), $this->getId() ) . "\n";
 252+ foreach ( $this->getAllProperties() as $name => $value ) {
 253+ $s .= Xml::element( 'property', array( 'name' => $name ), $value ) . "\n";
 254+ }
 255+ foreach ( $this->getMessageNames() as $name ) {
 256+ foreach ( self::$languages as $lang ) {
 257+ $s .= Xml::element( 'message', array( 'name' => $name, 'lang' => $lang ),
 258+ $this->getRawMessage( $name, $lang ) ) . "\n";
 259+ }
 260+ }
 261+ return $s;
 262+ }
 263+
202264 }
Index: trunk/extensions/SecurePoll/includes/Ballot.php
@@ -14,11 +14,11 @@
1515 abstract function getTallyTypes();
1616
1717 /**
18 - * Get the HTML for this ballot. <form> tags should not be included,
19 - * they will be added by the VotePage.
 18+ * Get the HTML form segment for a single question
 19+ * @param $question SecurePoll_Question
2020 * @return string
2121 */
22 - abstract function getForm();
 22+ abstract function getQuestionForm( $question );
2323
2424 /**
2525 * Called when the form is submitted. This returns a Status object which,
@@ -58,6 +58,28 @@
5959 function __construct( $election ) {
6060 $this->election = $election;
6161 }
 62+
 63+ /**
 64+ * Get the HTML for this ballot. <form> tags should not be included,
 65+ * they will be added by the VotePage.
 66+ * @return string
 67+ */
 68+ function getForm() {
 69+ global $wgParser, $wgTitle;
 70+ $questions = $this->election->getQuestions();
 71+ if ( $this->election->getProperty( 'shuffle-questions' ) ) {
 72+ shuffle( $questions );
 73+ }
 74+
 75+ $s = '';
 76+ foreach ( $questions as $question ) {
 77+ $s .= "<hr/>\n" .
 78+ $question->parseMessage( 'text' ) .
 79+ $this->getQuestionForm( $question ) .
 80+ "\n";
 81+ }
 82+ return $s;
 83+ }
6284 }
6385
6486 /**
@@ -79,37 +101,25 @@
80102 }
81103
82104 /**
83 - * Get the HTML for this ballot.
 105+ * Get the HTML form segment for a single question
 106+ * @param $question SecurePoll_Question
84107 * @return string
85108 */
86 - function getForm() {
87 - global $wgParser, $wgTitle;
88 - $questions = $this->election->getQuestions();
89 - if ( $this->election->getProperty( 'shuffle-questions' ) ) {
90 - shuffle( $questions );
 109+ function getQuestionForm( $question ) {
 110+ $options = $question->getChildren();
 111+ if ( $this->election->getProperty( 'shuffle-options' ) ) {
 112+ shuffle( $options );
91113 }
92 -
 114+ $name = 'securepoll_q' . $question->getId();
93115 $s = '';
94 - $parserOpts = new ParserOptions;
95 -
96 - foreach ( $questions as $question ) {
97 - $s .= "<hr/>\n";
98 - $s .= $wgParser->parse( $question->getMessage( 'text' ), $wgTitle, $parserOpts )->getText();
99 - $options = $question->getChildren();
100 - if ( $this->election->getProperty( 'shuffle-options' ) ) {
101 - shuffle( $options );
102 - }
103 - $name = 'securepoll_q' . $question->getId();
104 - foreach ( $options as $option ) {
105 - $optionText = $option->getMessage( 'text' );
106 - $optionHTML = $wgParser->parse( $optionText, $wgTitle, $parserOpts, false )->getText();
107 - $optionId = $option->getId();
108 - $radioId = "{$name}_opt{$optionId}";
109 - $s .= Xml::radio( $name, $optionId, false, array( 'id' => $radioId ) ) .
110 - '&nbsp;' .
111 - Xml::tags( 'label', array( 'for' => $radioId ), $optionText ) .
112 - "<br/>\n";
113 - }
 116+ foreach ( $options as $option ) {
 117+ $optionHTML = $option->parseMessageInline( 'text' );
 118+ $optionId = $option->getId();
 119+ $radioId = "{$name}_opt{$optionId}";
 120+ $s .= Xml::radio( $name, $optionId, false, array( 'id' => $radioId ) ) .
 121+ '&nbsp;' .
 122+ Xml::tags( 'label', array( 'for' => $radioId ), $optionHTML ) .
 123+ "<br/>\n";
114124 }
115125 return $s;
116126 }
@@ -154,61 +164,99 @@
155165 }
156166
157167 /**
158 - * TODO: this is code copied directly from BoardVote, it needs to be ported.
 168+ * Ballot for preferential voting
 169+ * Properties:
 170+ * shuffle-questions
 171+ * shuffle-options
 172+ * must-rank-all
159173 */
160174 class SecurePoll_PreferentialBallot extends SecurePoll_Ballot {
161175 function getTallyTypes() {
162 - return array( 'plurality', 'condorcet' );
 176+ return array( 'schulze' );
163177 }
164178
165 - function getRecord() {
166 - global $wgBoardCandidates;
167 -
168 - $record = "I prefer: ";
169 - $num_candidates = count( $wgBoardCandidates );
170 - $cnt = 0;
171 - foreach ( $this->mVotedFor as $i => $rank ) {
172 - $cnt++;
173 -
174 - $record .= $wgBoardCandidates[ $i ] . "[";
175 - $record .= ( $rank == '' ) ? 100 : $rank;
176 - $record .= "]";
177 - $record .= ( $cnt != $num_candidates ) ? ", " : "";
 179+ function getQuestionForm( $question ) {
 180+ global $wgRequest;
 181+ $options = $question->getChildren();
 182+ if ( $this->election->getProperty( 'shuffle-options' ) ) {
 183+ shuffle( $options );
178184 }
179 - $record .= "\n";
 185+ $name = 'securepoll_q' . $question->getId();
 186+ $s = '';
 187+ foreach ( $options as $option ) {
 188+ $optionHTML = $option->parseMessageInline( 'text' );
 189+ $optionId = $option->getId();
 190+ $inputId = "{$name}_opt{$optionId}";
 191+ $oldValue = $wgRequest->getVal( $inputId, '' );
 192+ $s .=
 193+ Xml::input( $inputId, '3', $oldValue, array(
 194+ 'id' => $inputId,
 195+ 'maxlength' => 3,
 196+ ) ) .
 197+ '&nbsp;' .
 198+ Xml::tags( 'label', array( 'for' => $inputId ), $optionHTML ) .
 199+ '&nbsp;' .
 200+ "<br/>\n";
 201+ }
 202+ return $s;
 203+ }
180204
181 - // Pad it out with spaces to a constant length, so that the encrypted record is secure
182 - $padLength = array_sum( array_map( 'strlen', $wgBoardCandidates ) ) + $num_candidates * 8 + 20;
183 - // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^
184 - // length of the candidate names added together room for rank & separators extra
 205+ function submitForm() {
 206+ global $wgRequest;
 207+ $questions = $this->election->getQuestions();
 208+ $record = '';
 209+ $status = Status::newGood();
185210
186 - $record = str_pad( $record, $padLength );
187 - return $record;
188 - }
 211+ foreach ( $questions as $question ) {
 212+ $options = $question->getOptions();
 213+ foreach ( $options as $option ) {
 214+ $id = 'securepoll_q' . $question->getId() . '_opt' . $option->getId();
 215+ $rank = $wgRequest->getVal( $id );
189216
190 - function validVote() {
191 - foreach ( $this->mVotedFor as $rank ) {
192 - if ( $rank != '' ) {
193 - if ( !preg_match( '/^[1-9]\d?$/', $rank ) ) {
194 - return false;
 217+ if ( is_numeric( $rank ) ) {
 218+ if ( $rank <= 0 || $rank >= 1000 ) {
 219+ $status->fatal( 'securepoll-invalid-rank', $id );
 220+ continue;
 221+ } else {
 222+ $rank = intval( $rank );
 223+ }
 224+ } elseif ( strval( $rank ) === '' ) {
 225+ if ( $this->election->getProperty( 'must-rank-all' ) ) {
 226+ $status->fatal( 'securepoll-unranked-options', $id );
 227+ continue;
 228+ } else {
 229+ $rank = 1000;
 230+ }
 231+ } else {
 232+ $status->fatal( 'securepoll-invalid-rank', $id );
 233+ continue;
195234 }
 235+ $record .= sprintf( 'Q%08X-A%08X-R%08X--',
 236+ $question->getId(), $option->getId(), $rank );
196237 }
197238 }
198 -
199 - return true;
 239+ if ( $status->isOK() ) {
 240+ $status->value = $record . "\n";
 241+ }
 242+ return $status;
200243 }
201244
202 - function voteEntry( $index, $candidate ) {
203 - return "
204 - <tr><td align=\"right\">
205 - <input type=\"text\" maxlength=\"2\" size=\"2\" name=\"candidate[{$index}]\" />
206 - </td><td align=\"left\">
207 - $candidate
208 - </td></tr>";
 245+ function unpackRecord( $record ) {
 246+ $ranks = array();
 247+ $itemLength = 3*8 + 7;
 248+ for ( $offset = 0; $offset < strlen( $record ); $offset += $itemLength ) {
 249+ if ( !preg_match( '/Q([0-9A-F]{8})-A([0-9A-F]{8})-R([0-9A-F]{8})--/A',
 250+ $record, $m, 0, $offset ) )
 251+ {
 252+ wfDebug( __METHOD__.": regex doesn't match\n" );
 253+ return false;
 254+ }
 255+ $qid = intval( base_convert( $m[1], 16, 10 ) );
 256+ $oid = intval( base_convert( $m[2], 16, 10 ) );
 257+ $rank = intval( base_convert( $m[3], 16, 10 ) );
 258+ $ranks[$qid][$oid] = $rank;
 259+ }
 260+ return $ranks;
209261 }
210 -
211 - function getForm() { }
212 - function submitForm() { }
213 - function unpackRecord( $record ) {}
214262 }
215263
Index: trunk/extensions/SecurePoll/includes/Random.php
@@ -0,0 +1,70 @@
 2+<?php
 3+
 4+class SecurePoll_Random {
 5+ var $urandom;
 6+
 7+ /**
 8+ * Open a /dev/urandom file handle
 9+ * Returns a Status object
 10+ */
 11+ function open() {
 12+ if ( $this->urandom ) {
 13+ return Status::newGood();
 14+ }
 15+
 16+ if ( wfIsWindows() ) {
 17+ return Status::newFatal( 'securepoll-urandom-not-supported' );
 18+ }
 19+ $this->urandom = fopen( '/dev/urandom', 'rb' );
 20+ if ( !$this->urandom ) {
 21+ return Status::newFatal( 'securepoll-dump-no-urandom' );
 22+ }
 23+ return Status::newGood();
 24+ }
 25+
 26+ /**
 27+ * Close any open file handles
 28+ */
 29+ function close() {
 30+ if ( $this->urandom ) {
 31+ fclose( $this->urandom );
 32+ $this->urandom = null;
 33+ }
 34+ }
 35+
 36+ /**
 37+ * Get a random integer between 0 and ($maxp1 - 1).
 38+ * Should only be called after open() succeeds.
 39+ */
 40+ function getInt( $maxp1 ) {
 41+ $numBytes = ceil( strlen( base_convert( $maxp1, 10, 16 ) ) / 2 );
 42+ if ( $numBytes == 0 ) {
 43+ return 0;
 44+ }
 45+ $data = fread( $this->urandom, $numBytes );
 46+ if ( strlen( $data ) != $numBytes ) {
 47+ throw new MWException( __METHOD__.': not enough bytes' );
 48+ }
 49+ $x = 0;
 50+ for ( $i = 0; $i < $numBytes; $i++ ) {
 51+ $x *= 256;
 52+ $x += ord( substr( $data, $i, 1 ) );
 53+ }
 54+ return $x % $maxp1;
 55+ }
 56+
 57+ /**
 58+ * Works like shuffle() except more secure. Returns the new array instead
 59+ * of modifying it. The algorithm is the Knuth/Durstenfeld kind.
 60+ */
 61+ function shuffle( $a ) {
 62+ $a = array_values( $a );
 63+ for ( $i = count( $a ) - 1; $i >= 0; $i-- ) {
 64+ $target = $this->getInt( $i + 1 );
 65+ $tmp = $a[$i];
 66+ $a[$i] = $a[$target];
 67+ $a[$target] = $tmp;
 68+ }
 69+ return $a;
 70+ }
 71+}
Property changes on: trunk/extensions/SecurePoll/includes/Random.php
___________________________________________________________________
Added: svn:eol-style
172 + native
Index: trunk/extensions/SecurePoll/includes/TallyPage.php
@@ -17,7 +17,7 @@
1818 }
1919
2020 $electionId = intval( $params[0] );
21 - $this->election = $this->parent->getElection( $electionId );
 21+ $this->election = SecurePoll::getElection( $electionId );
2222 if ( !$this->election ) {
2323 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2424 return;
@@ -136,19 +136,46 @@
137137 ), __METHOD__
138138 );
139139 $crypt = $this->election->getCrypt();
140 - $tallier = $this->election->getTallier();
 140+ $ballot = $this->election->getBallot();
 141+ $questions = $this->election->getQuestions();
 142+ $talliers = $this->election->getTalliers();
141143 foreach ( $res as $row ) {
142 - $status = $crypt->decrypt( $row->vote_record );
143 - if ( !$status->isOK() ) {
144 - $wgOut->addWikiText( $status->getWikiText() );
145 - return;
 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;
146152 }
147 - if ( !$tallier->addRecord( $status->value ) ) {
148 - $wgOut->addWikiMsg( 'securepoll-tally-error' );
149 - return;
 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+ }
150165 }
151166 }
152 - $wgOut->addHTML( $tallier->getResult() );
 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+ }
153180 }
154181
155182 /**
@@ -182,7 +209,8 @@
183210 return;
184211 }
185212 }
186 - $wgOut->addHTML( $tallier->getResult() );
 213+ $tallier->finishTally();
 214+ $wgOut->addHTML( $tallier->getHtmlResult() );
187215 }
188216
189217 function getTitle() {
Index: trunk/extensions/SecurePoll/includes/TranslatePage.php
@@ -17,7 +17,7 @@
1818 }
1919
2020 $electionId = intval( $params[0] );
21 - $this->election = $this->parent->getElection( $electionId );
 21+ $this->election = SecurePoll::getElection( $electionId );
2222 if ( !$this->election ) {
2323 $wgOut->addWikiMsg( 'securepoll-invalid-election', $electionId );
2424 return;
Index: trunk/extensions/SecurePoll/includes/Tallier.php
@@ -1,22 +1,27 @@
22 <?php
33
44 abstract class SecurePoll_Tallier {
5 - var $election;
 5+ var $question;
66
7 - abstract function addRecord( $record );
8 - abstract function getResult();
 7+ abstract function addVote( $scores );
 8+ abstract function getHtmlResult();
 9+ abstract function getTextResult();
910
10 - static function factory( $type, $election ) {
 11+ abstract function finishTally();
 12+
 13+ static function factory( $type, $question ) {
1114 switch ( $type ) {
1215 case 'plurality':
13 - return new SecurePoll_PluralityTallier( $election );
 16+ return new SecurePoll_PluralityTallier( $question );
 17+ case 'schulze':
 18+ return new SecurePoll_SchulzeTallier( $question );
1419 default:
1520 throw new MWException( "Invalid tallier type: $type" );
1621 }
1722 }
1823
19 - function __construct( $election ) {
20 - $this->election = $election;
 24+ function __construct( $question ) {
 25+ $this->question = $question;
2126 }
2227 }
2328
@@ -26,63 +31,204 @@
2732 class SecurePoll_PluralityTallier extends SecurePoll_Tallier {
2833 var $tally = array();
2934
30 - function __construct( $election ) {
31 - parent::__construct( $election );
32 - $questions = $this->election->getQuestions();
33 - foreach ( $questions as $question ) {
34 - foreach ( $question->getOptions() as $option ) {
35 - $this->tally[$question->getId()][$option->getId()] = 0;
 35+ function __construct( $question ) {
 36+ parent::__construct( $question );
 37+ foreach ( $question->getOptions() as $option ) {
 38+ $this->tally[$option->getId()] = 0;
 39+ }
 40+ }
 41+
 42+ function addVote( $scores ) {
 43+ foreach ( $scores as $oid => $score ) {
 44+ if ( !isset( $this->tally[$oid] ) ) {
 45+ wfDebug( __METHOD__.": unknown OID $oid\n" );
 46+ return false;
3647 }
 48+ $this->tally[$oid] += $score;
3749 }
 50+ return true;
3851 }
3952
40 - function addRecord( $record ) {
41 - $i = 0;
42 - $ballot = $this->election->getBallot();
43 - $scores = $ballot->unpackRecord( $record );
44 - if ( $scores === false ) {
45 - return false;
 53+ function finishTally() {
 54+ // Sort the scores
 55+ arsort( $this->tally );
 56+ }
 57+
 58+ function getHtmlResult() {
 59+ // Show the results
 60+ $s = '';
 61+
 62+ foreach ( $this->question->getOptions() as $option ) {
 63+ $s .= '<tr><td>' . $option->getMessage( 'text' ) . "</td>\n" .
 64+ '<td>' . $this->tally[$option->getId()] . "</td>\n" .
 65+ "</tr>\n";
4666 }
47 - foreach ( $scores as $qid => $questionScores ) {
48 - if ( !isset( $this->tally[$qid] ) ) {
49 - wfDebug( __METHOD__.": unknown QID $qid\n" );
 67+ $s .= "</table>\n";
 68+ return $s;
 69+ }
 70+
 71+ function getTextResult() {
 72+ // Calculate column width
 73+ $width = 10;
 74+ foreach ( $question->getOptions() as $option ) {
 75+ $width = max( $width, strlen( $option->getMessage( 'text' ) ) );
 76+ }
 77+ if ( $width > 57 ) {
 78+ $width = 57;
 79+ }
 80+
 81+ // Show the results
 82+ $s = wordwrap( $question->getMessage( 'text' ) ) . "\n";
 83+ foreach ( $question->getOptions() as $option ) {
 84+ $otext = $option->getMessage( 'text' );
 85+ if ( strlen( $otext ) > $width ) {
 86+ $otext = substr( $otext, 0, $width - 3 ) . '...';
 87+ } else {
 88+ $otext = str_pad( $otext, $width );
 89+ }
 90+ $s .= $otext . ' | ' .
 91+ $this->tally[$question->getId()][$option->getId()] . "\n";
 92+ }
 93+ return $s;
 94+ }
 95+
 96+ function getRanks() {
 97+ $ranks = array();
 98+ $currentRank = 1;
 99+ $oids = array_keys( $this->tally );
 100+ $scores = array_values( $this->tally );
 101+ foreach ( $oids as $i => $oid ) {
 102+ if ( $i > 0 && $scores[$i-1] !== $scores[$i] ) {
 103+ $currentRank = $i + 1;
 104+ }
 105+ $ranks[$oid] = $currentRank;
 106+ }
 107+ return $ranks;
 108+ }
 109+}
 110+
 111+abstract class SecurePoll_PairwiseTallier extends SecurePoll_Tallier {
 112+ var $optionIds = array();
 113+ var $victories = array();
 114+
 115+ function __construct( $question ) {
 116+ parent::__construct( $question );
 117+ $this->optionIds = array();
 118+ foreach ( $question->getOptions() as $option ) {
 119+ $this->optionIds[] = $option->getId();
 120+ }
 121+
 122+ $this->victories = array();
 123+ foreach ( $this->optionIds as $i ) {
 124+ foreach ( $this->optionIds as $j ) {
 125+ $this->victories[$i][$j] = 0;
 126+ }
 127+ }
 128+ }
 129+
 130+ function addVote( $ranks ) {
 131+ foreach ( $this->optionIds as $oid1 ) {
 132+ if ( !isset( $ranks[$oid1] ) ) {
 133+ wfDebug( "Invalid vote record, missing option $oid1\n" );
50134 return false;
51135 }
52 - foreach ( $questionScores as $oid => $score ) {
53 - if ( !isset( $this->tally[$qid][$oid] ) ) {
54 - wfDebug( __METHOD__.": unknown OID $oid\n" );
55 - return false;
 136+ foreach ( $this->optionIds as $oid2 ) {
 137+ # Lower = better
 138+ if ( $ranks[$oid1] < $ranks[$oid2] ) {
 139+ $this->victories[$oid1][$oid2]++;
56140 }
57 - $this->tally[$qid][$oid] += $score;
58141 }
59142 }
60143 return true;
61144 }
 145+}
62146
63 - function getResult() {
64 - global $wgOut;
65 - $questions = $this->election->getQuestions();
 147+/**
 148+ * This is the basic Schulze method with no tie-breaking.
 149+ */
 150+class SecurePoll_SchulzeTallier extends SecurePoll_PairwiseTallier {
 151+ var $strengths;
 152+
 153+ function finishTally() {
 154+ # This algorithm follows Markus Schulze, "A New Monotonic, Clone-Independent, Reversal
 155+ # Symmetric, and Condorcet-Consistent Single-Winner Election Method"
66156
67 - // Sort the scores
68 - foreach ( $this->tally as &$scores ) {
69 - arsort( $scores );
 157+ $this->strengths = array();
 158+ foreach ( $this->optionIds as $oid1 ) {
 159+ foreach ( $this->optionIds as $oid2 ) {
 160+ if ( $oid1 === $oid2 ) {
 161+ continue;
 162+ }
 163+ if ( $this->victories[$oid1][$oid2] > $this->victories[$oid2][$oid1] ) {
 164+ $this->strengths[$oid1][$oid2] = $this->victories[$oid1][$oid2];
 165+ } else {
 166+ $this->strengths[$oid1][$oid2] = 0;
 167+ }
 168+ }
70169 }
71170
72 - // Show the results
73 - $s = '';
74 - foreach ( $questions as $question ) {
75 - if ( $s !== '' ) {
76 - $s .= "<hr/>\n";
 171+ foreach ( $this->optionIds as $oid1 ) {
 172+ foreach ( $this->optionIds as $oid2 ) {
 173+ if ( $oid1 === $oid2 ) {
 174+ continue;
 175+ }
 176+ foreach ( $this->optionIds as $oid3 ) {
 177+ if ( $oid1 === $oid3 || $oid2 === $oid3 ) {
 178+ continue;
 179+ }
 180+ $this->strengths[$oid2][$oid3] = max(
 181+ $this->strengths[$oid2][$oid3],
 182+ min(
 183+ $this->strengths[$oid2][$oid1],
 184+ $this->strengths[$oid1][$oid3]
 185+ )
 186+ );
 187+ }
77188 }
78 - $s .= $wgOut->parse( $question->getMessage( 'text' ) ) .
79 - '<table class="securepoll-result-table" border="1">';
80 - foreach ( $question->getOptions() as $option ) {
81 - $s .= '<tr><td>' . $option->getMessage( 'text' ) . "</td>\n" .
82 - '<td>' . $this->tally[$question->getId()][$option->getId()] . "</td>\n" .
83 - "</tr>\n";
 189+ }
 190+
 191+ # Calculate ranks
 192+ $this->ranks = array();
 193+ $rankedOptions = $this->optionIds;
 194+ usort( $rankedOptions, array( $this, 'comparePair' ) );
 195+ $rankedOptions = array_reverse( $rankedOptions );
 196+ $currentRank = 1;
 197+ foreach ( $rankedOptions as $i => $oid ) {
 198+ if ( $i > 0 && $this->comparePair( $rankedOptions[$i-1], $oid ) ) {
 199+ $currentRank = $i + 1;
84200 }
85 - $s .= "</table>\n";
 201+ $this->ranks[$oid] = $currentRank;
86202 }
87 - return $s;
88203 }
 204+
 205+ function comparePair( $i, $j ) {
 206+ if ( $i === $j ) {
 207+ return 0;
 208+ }
 209+ $sij = $this->strengths[$i][$j];
 210+ $sji = $this->strengths[$j][$i];
 211+ if ( $sij > $sji ) {
 212+ return 1;
 213+ } elseif ( $sji > $sij ) {
 214+ return -1;
 215+ } else {
 216+ return 0;
 217+ }
 218+ }
 219+
 220+ function getHtmlResult() {
 221+ return '<pre>' . $this->getTextResult() . '</pre>';
 222+
 223+ }
 224+
 225+ function getTextResult() {
 226+ return
 227+ "Victory matrix:\n" .
 228+ var_export( $this->victories, true ) . "\n\n" .
 229+ "Path strength matrix:\n" .
 230+ var_export( $this->strengths, true ) . "\n\n" .
 231+ "Ranks:\n" .
 232+ var_export( $this->ranks, true ) . "\n";
 233+ }
89234 }
 235+
Index: trunk/extensions/SecurePoll/cli/dump.php
@@ -0,0 +1,65 @@
 2+<?php
 3+
 4+/**
 5+ * Generate an XML dump of an election, including configuration and votes.
 6+ */
 7+
 8+
 9+$optionsWithArgs = array( 'o' );
 10+require( dirname(__FILE__).'/cli.inc' );
 11+
 12+if ( !isset( $args[0] ) ) {
 13+ spFatal( "Usage: php dump.php [-o <outfile>] <election name>" );
 14+}
 15+
 16+$election = SecurePoll::getElectionByTitle( $args[0] );
 17+if ( !$election ) {
 18+ spFatal( "There is no election called \"$args[0]\"" );
 19+}
 20+
 21+if ( !isset( $options['o'] ) ) {
 22+ $fileName = '-';
 23+} else {
 24+ $fileName = $options['o'];
 25+}
 26+if ( $fileName === '-' ) {
 27+ $outFile = STDIN;
 28+} else {
 29+ $outFile = fopen( $fileName, 'w' );
 30+}
 31+if ( !$outFile ) {
 32+ spFatal( "Unable to open $fileName for writing" );
 33+}
 34+
 35+SecurePoll_Entity::setLanguages( array( $election->getLanguage() ) );
 36+
 37+$cbdata = array(
 38+ 'header' => "<SecurePoll>\n<election>\n" . $election->getConfXml(),
 39+ 'outFile' => $outFile
 40+);
 41+
 42+# Write vote records
 43+$election->cbdata = $cbdata;
 44+$status = $election->dumpVotesToCallback( 'spDumpVote' );
 45+if ( !$status->isOK() ) {
 46+ spFatal( $status->getWikiText() );
 47+}
 48+if ( $election->cbdata['header'] ) {
 49+ echo $election->cbdata['header'];
 50+}
 51+
 52+fwrite( $outFile, "</election>\n</SecurePoll>\n" );
 53+
 54+function spFatal( $message ) {
 55+ fwrite( STDERR, rtrim( $message ) . "\n" );
 56+ exit( 1 );
 57+}
 58+
 59+function spDumpVote( $election, $row ) {
 60+ if ( $election->cbdata['header'] ) {
 61+ echo $election->cbdata['header'];
 62+ $election->cbdata['header'] = false;
 63+ }
 64+ fwrite( $election->cbdata['outFile'], "<vote>" . $row->vote_record . "</vote>\n" );
 65+}
 66+
Property changes on: trunk/extensions/SecurePoll/cli/dump.php
___________________________________________________________________
Added: svn:eol-style
167 + native
Index: trunk/extensions/SecurePoll/cli/tally.php
@@ -0,0 +1,107 @@
 2+<?php
 3+
 4+/**
 5+ * Tally an election from a dump file or local database.
 6+ *
 7+ * Can be used to tally very large numbers of votes, when the web interface is
 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+ */
 14+
 15+$optionsWithArgs = array( 'name' );
 16+require( dirname(__FILE__).'/cli.inc' );
 17+
 18+$usage = <<<EOT
 19+Usage:
 20+ php tally.php [--html] --name <election name>
 21+ php tally.php [--html] <dump file>
 22+
 23+EOT;
 24+
 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 );
 31+}
 32+
 33+if ( !class_exists( 'SecurePoll' ) ) {
 34+ # Uninstalled mode
 35+ # This may actually work some day, for now it will just give you DB errors
 36+ require( dirname( __FILE__ ) . '/../SecurePoll.php' );
 37+}
 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+
 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+ }
 95+ }
 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();
 107+ }
 108+}
Property changes on: trunk/extensions/SecurePoll/cli/tally.php
___________________________________________________________________
Added: svn:eol-style
1109 + native
Index: trunk/extensions/SecurePoll/cli/cli.inc
@@ -0,0 +1,16 @@
 2+<?php
 3+
 4+$IP = getenv( 'MW_INSTALL_PATH' );
 5+if ( strval( $IP ) === '' ) {
 6+ $IP = dirname( __FILE__ ).'/../../..';
 7+}
 8+if ( !file_exists( "$IP/includes/WebStart.php" ) ) {
 9+ $IP .= '/phase3';
 10+}
 11+if ( !file_exists( "$IP/includes/WebStart.php" ) ) {
 12+ echo "Can't find your MediaWiki installation. Please set the MW_INSTALL_PATH environment variable.\n";
 13+ exit( 1 );
 14+}
 15+
 16+require( $IP . '/maintenance/commandLine.inc' );
 17+
Property changes on: trunk/extensions/SecurePoll/cli/cli.inc
___________________________________________________________________
Added: svn:eol-style
118 + native

Status & tagging log