Introducing special page modular extensions, making the board vote special page the...
[lhc/web/wiklou.git] / extensions / BoardVote.php
1 <?php
2
3 # Wikimedia Foundation Board of Trustees Election
4
5 # Register extension
6 $wgExtensionFunctions[] = "wfBoardvoteSetup";
7
8
9 function wfBoardvoteSetup()
10 {
11 # Look out, freaky indenting
12 # All this happens after SpecialPage.php has been included in Setup.php
13
14 class BoardVotePage extends SpecialPage {
15 var $mPosted, $mContributing, $mVolunteer, $mDBname, $mUserDays, $mUserEdits;
16 var $mHasVoted, $mAction, $mUserKey;
17
18 function BoardVotePage() {
19 SpecialPage::SpecialPage( "Boardvote" );
20 }
21
22 function execute( $par ) {
23 global $wgUser, $wgDBname, $wgInputEncoding, $wgRequest, $wgBoardVoteDB;
24
25 $this->mUserKey = iconv( $wgInputEncoding, "UTF-8", $wgUser->getName() ) . "@$wgDBname";
26 $this->mPosted = $wgRequest->wasPosted();
27 $this->mContributing = $wgRequest->getInt( "contributing" );
28 $this->mVolunteer = $wgRequest->getInt( "volunteer" );
29 $this->mDBname = $wgBoardVoteDB;
30 $this->mHasVoted = $this->hasVoted( $wgUser );
31
32 if ( $par ) {
33 $this->mAction = $par;
34 } else {
35 $this->mAction = $wgRequest->getText( "action" );
36 }
37
38 $this->setHeaders();
39
40 if ( $this->mAction == "list" ) {
41 $this->displayList();
42 } elseif ( $this->mAction == "dump" ) {
43 $this->dump();
44 } elseif( $this->mAction == "vote" ) {
45 if ( !$wgUser->getID() ) {
46 $this->notLoggedIn();
47 } else {
48 $this->getQualifications( $wgUser );
49 if ( $this->mUserDays < 90 ) {
50 $this->notQualified();
51 } elseif ( $this->mPosted ) {
52 $this->logVote();
53 } else {
54 $this->displayVote();
55 }
56 }
57 } else {
58 $this->displayEntry();
59 }
60 }
61
62 function displayEntry() {
63 global $wgOut;
64 $wgOut->addWikiText( wfMsg( "boardvote_entry" ) );
65 }
66
67 function hasVoted( &$user ) {
68 global $wgDBname;
69 $row = wfGetArray( $this->mDBname . ".log", array( "1" ),
70 array( "log_user_key" => $this->mUserKey ), "BoardVotePage::getUserVote" );
71 if ( $row === false ) {
72 return false;
73 } else {
74 return true;
75 }
76 }
77
78 function logVote() {
79 global $wgUser, $wgDBname, $wgIP, $wgOut;
80 $fname = "BoardVotePage::logVote";
81
82 $now = wfTimestampNow();
83 $record = $this->encrypt( $this->mContributing, $this->mVolunteer );
84 $db = $this->mDBname;
85
86 # Mark previous votes as old
87 $encKey = wfStrencode( $this->mUserKey );
88 $sql = "UPDATE $db.log SET log_current=0 WHERE log_user_key='$encKey'";
89 wfQuery( $sql, DB_WRITE, $fname );
90
91 # Add vote to log
92 wfInsertArray( "$db.log", array(
93 "log_user" => $wgUser->getID(),
94 "log_user_text" => $wgUser->getName(),
95 "log_user_key" => $this->mUserKey,
96 "log_wiki" => $wgDBname,
97 "log_edits" => $this->mUserEdits,
98 "log_days" => $this->mUserDays,
99 "log_record" => $record,
100 "log_ip" => $wgIP,
101 "log_xff" => @$_SERVER['HTTP_X_FORWARDED_FOR'],
102 "log_ua" => $_SERVER['HTTP_USER_AGENT'],
103 "log_timestamp" => $now,
104 "log_current" => 1
105 ), $fname );
106
107 $wgOut->addWikiText( wfMsg( "boardvote_entered", $record ) );
108 }
109
110 function displayVote() {
111 global $wgContributingCandidates, $wgVolunteerCandidates, $wgOut;
112
113 $thisTitle = Title::makeTitle( NS_SPECIAL, "Boardvote" );
114 $action = $thisTitle->getLocalURL( "action=vote" );
115 if ( $this->mHasVoted ) {
116 $intro = wfMsg( "boardvote_intro_change" );
117 } else {
118 $intro = wfMsg( "boardvote_intro" );
119 }
120 $contributing = wfMsg( "boardvote_contributing" );
121 $volunteer = wfMsg( "boardvote_volunteer" );
122 $ok = wfMsg( "ok" );
123
124 $candidatesV = $candidatesC = array();
125 foreach( $wgContributingCandidates as $i => $candidate ) {
126 $candidatesC[] = array( $i, $candidate );
127 }
128 foreach ( $wgVolunteerCandidates as $i => $candidate ) {
129 $candidatesV[] = array( $i, $candidate );
130 }
131
132 srand ((float)microtime()*1000000);
133 shuffle( $candidatesC );
134 shuffle( $candidatesV );
135
136 $text = "
137 $intro
138 <form name=\"boardvote\" id=\"boardvote\" method=\"post\" action=\"$action\">
139 <table border='0'><tr><td colspan=2>
140 <h2>$contributing</h2>
141 </td></tr>";
142 $text .= $this->voteEntry( -1, wfMsg( "boardvote_abstain" ), "contributing" );
143 foreach ( $candidatesC as $candidate ) {
144 $text .= $this->voteEntry( $candidate[0], $candidate[1], "contributing" );
145 }
146 $text .= "
147 <tr><td colspan=2>
148 <h2>$volunteer</h2></td></tr>";
149 $text .= $this->voteEntry( -1, wfMsg( "boardvote_abstain" ), "volunteer" );
150 foreach ( $candidatesV as $candidate ) {
151 $text .= $this->voteEntry( $candidate[0], $candidate[1], "volunteer" );
152 }
153
154 $text .= "<tr><td>&nbsp;</td><td>
155 <input name=\"submit\" type=\"submit\" value=\"$ok\">
156 </td></tr></table></form>";
157 $text .= wfMsg( "boardvote_footer" );
158 $wgOut->addHTML( $text );
159 }
160
161 function voteEntry( $index, $candidate, $name ) {
162 if ( $index == -1 ) {
163 $checked = " CHECKED";
164 } else {
165 $checked = "";
166 }
167
168 return "
169 <tr><td align=\"right\">
170 <input type=\"radio\" name=\"$name\" value=\"$index\"$checked>
171 </td><td align=\"left\">
172 $candidate
173 </td></tr>";
174 }
175
176 function notLoggedIn() {
177 global $wgOut;
178 $wgOut->addWikiText( wfMsg( "boardvote_notloggedin" ) );
179 }
180
181 function notQualified() {
182 global $wgOut;
183 $wgOut->addWikiText( wfMsg( "boardvote_notqualified", $this->mUserDays ) );
184 }
185
186 function encrypt( $contributing, $volunteer ) {
187 global $wgVolunteerCandidates, $wgContributingCandidates;
188 global $wgGPGCommand, $wgGPGRecipient, $wgGPGHomedir;
189 $file = @fopen( "/dev/urandom", "r" );
190 if ( $file ) {
191 $salt = implode( "", unpack( "H*", fread( $file, 64 ) ));
192 fclose( $file );
193 } else {
194 $salt = Parser::getRandomString() . Parser::getRandomString();
195 }
196 $record =
197 "Contributing: $contributing (" .$wgContributingCandidates[$contributing] . ")\n" .
198 "Volunteer: $volunteer (" . $wgVolunteerCandidates[$volunteer] . ")\n" .
199 "Salt: $salt\n";
200 # Get file names
201 $input = tempnam( "/tmp", "gpg_" );
202 $output = tempnam( "/tmp", "gpg_" );
203
204 # Write unencrypted record
205 $file = fopen( $input, "w" );
206 fwrite( $file, $record );
207 fclose( $file );
208
209 # Call GPG
210 $command = wfEscapeShellArg( $wgGPGCommand ) . " --batch --yes -ear " .
211 wfEscapeShellArg( $wgGPGRecipient ) . " -o " . wfEscapeShellArg( $output );
212 if ( $wgGPGHomedir ) {
213 $command .= " --homedir " . wfEscapeShellArg( $wgGPGHomedir );
214 }
215 $command .= " " . wfEscapeShellArg( $input );
216
217 shell_exec( $command );
218
219 # Read result
220 $result = file_get_contents( $output );
221
222 # Delete temporary files
223 unlink( $input );
224 unlink( $output );
225
226 return $result;
227 }
228
229 function getQualifications( &$user ) {
230 $id = $user->getID();
231 if ( !$id ) {
232 $this->mUserDays = 0;
233 $this->mUserEdits = 0;
234 return;
235 }
236
237 # Count contributions and find earliest edit
238 # First cur
239 $sql = "SELECT COUNT(*) as n, MIN(cur_timestamp) as t FROM cur WHERE cur_user=$id";
240 $res = wfQuery( $sql, DB_READ, "BoardVotePage::getQualifications" );
241 $cur = wfFetchObject( $res );
242 wfFreeResult( $res );
243
244 # If the user has stacks of contributions, don't check old as well
245 $now = time();
246 if ( is_null( $cur->t ) ) {
247 $signup = $now;
248 } else {
249 $signup = wfTimestamp2Unix( $cur->t );
250 }
251
252 $days = ($now - $signup) / 86400;
253 if ( $cur->n > 400 && $days > 180 ) {
254 $this->mUserDays = 0x7fffffff;
255 $this->mUserEdits = 0x7fffffff;
256 return;
257 }
258
259 # Now check old
260 $sql = "SELECT COUNT(*) as n, MIN(old_timestamp) as t FROM old WHERE old_user=$id";
261 $res = wfQuery( $sql, DB_READ, "BoardVotePage::getQualifications" );
262 $old = wfFetchObject( $res );
263 wfFreeResult( $res );
264
265 if ( !is_null( $old->t ) ) {
266 $signup = min( wfTimestamp2Unix( $old->t ), $signup );
267 }
268 $this->mUserDays = (int)(($now - $signup) / 86400);
269 $this->mUserEdits = $cur->n + $old->n;
270 }
271
272 function displayList() {
273 global $wgOut, $wgOutputEncoding, $wgLang, $wgUser;
274 $sql = "SELECT log_timestamp,log_user_key FROM {$this->mDBname}.log ORDER BY log_user_key";
275 $res = wfQuery( $sql, DB_READ, "BoardVotePage::list" );
276 if ( wfNumRows( $res ) == 0 ) {
277 $wgOut->addWikiText( wfMsg( "boardvote_novotes" ) );
278 return;
279 }
280 $thisTitle = Title::makeTitle( NS_SPECIAL, "Boardvote" );
281 $sk = $wgUser->getSkin();
282 $dumpLink = $sk->makeKnownLinkObj( $thisTitle, wfMsg( "boardvote_dumplink" ), "action=dump" );
283
284 $intro = wfMsg( "boardvote_listintro", $dumpLink );
285 $hTime = wfMsg( "boardvote_time" );
286 $hUser = wfMsg( "boardvote_user" );
287 $hContributing = wfMsg( "boardvote_contributing" );
288 $hVolunteer = wfMsg( "boardvote_volunteer" );
289
290 $s = "$intro <table border=1><tr><th>
291 $hUser
292 </th><th>
293 $hTime
294 </th></tr>";
295
296 while ( $row = wfFetchObject( $res ) ) {
297 if ( $wgOutputEncoding != "utf-8" ) {
298 $user = wfUtf8ToHTML( $row->log_user_key );
299 } else {
300 $user = $row->log_user_key;
301 }
302 $time = $wgLang->timeanddate( $row->log_timestamp );
303 $s .= "<tr><td>
304 $user
305 </td><td>
306 $time
307 </td></tr>";
308 }
309 $s .= "</table>";
310 $wgOut->addHTML( $s );
311 }
312
313 function dump() {
314 global $wgOut, $wgOutputEncoding, $wgLang, $wgUser;
315
316 $userRights = $wgUser->getRights();
317 if ( in_array( "boardvote", $userRights ) ) {
318 $admin = true;
319 } else {
320 $admin = false;
321 }
322
323 $sql = "SELECT * FROM {$this->mDBname}.log";
324 $res = wfQuery( $sql, DB_READ, "BoardVotePage::list" );
325 if ( wfNumRows( $res ) == 0 ) {
326 $wgOut->addWikiText( wfMsg( "boardvote_novotes" ) );
327 return;
328 }
329 $s = "<table border=1>";
330 if ( $admin ) {
331 $s .= wfMsg( "boardvote_dumpheader" );
332 }
333
334 while ( $row = wfFetchObject( $res ) ) {
335 if ( $wgOutputEncoding != "utf-8" ) {
336 $user = wfUtf8ToHTML( $row->log_user_key );
337 } else {
338 $user = $row->log_user_key;
339 }
340 $time = $wgLang->timeanddate( $row->log_timestamp );
341 $record = nl2br( $row->log_record );
342 if ( $admin ) {
343 $edits = $row->log_edits == 0x7fffffff ? "many" : $row->log_edits;
344 $days = $row->log_days == 0x7fffffff ? "many" : $row->log_days;
345 $s .= "<tr><td>
346 $time
347 </td><td>
348 $user
349 </td><td>
350 $edits
351 </td><td>
352 $days
353 </td><td>
354 {$row->log_ip}
355 </td><td>
356 {$row->log_ua}
357 </td><td>
358 $record
359 </td></tr>";
360 } else {
361 $s .= "<tr><td>$time</td><td>$user</td><td>$record</td></tr>";
362 }
363 }
364 $s .= "</table>";
365 $wgOut->addHTML( $s );
366 }
367 }
368
369 SpecialPage::addPage( new BoardVotePage );
370
371 global $wgMessageCache;
372 $wgMessageCache->addMessages( array(
373 # Board vote
374 "boardvote" => "Wikimedia Board of Trustees election",
375 "boardvote_entry" =>
376 "* [[Special:Boardvote/vote|Vote]]
377 * [[Special:Boardvote/list|List votes to date]]
378 * [[Special:Boardvote/dump|Dump encrypted election record]]",
379 "boardvote_intro" => "<p>Please choose your preferred candidate for both the
380 Contributing Representative and the Volunteer Representative.</p>",
381 "boardvote_intro_change" => "<p>You have voted before. However you may change
382 your vote using the form below.</p>",
383 "boardvote_abstain" => "Abstain",
384 "boardvote_footer" => "&nbsp;",
385 "boardvote_entered" => "Thank you, your vote has been recorded.
386
387 Following is your encrypted vote record. It will appear publicly at [[Special:Boardvote/dump]].
388
389 <pre>$1</pre>
390
391 [[Special:Boardvote/entry|Back]]",
392 "boardvote_notloggedin" => "You are not logged in. To vote, you must use an account
393 which has existed for at least 90 days.",
394 "boardvote_notqualified" => "Sorry, your first contribution was only $1 days ago.
395 You need to have been contributing for at least 90 days to vote in this election.",
396 "boardvote_novotes" => "Nobody has voted yet.",
397 "boardvote_contributing" => "Contributing candidate",
398 "boardvote_volunteer" => "Volunteer candidate",
399 "boardvote_time" => "Time",
400 "boardvote_user" => "User",
401 "boardvote_listintro" => "<p>This is a list of all votes which have been recorded
402 to date. $1 for the full encrypted election record.</p>",
403 "boardvote_dumplink" => "Click here",
404 "boardvote_dumpheader" => "<caption><strong>Election administrator private dump</strong></caption>
405 <tr><th>Time</th><th>User</th><th>Edits</th><th>Days</th>
406 <th>IP</th><th>User agent</th><th>Record</th></tr>"
407
408 ));
409
410 } # End of extension function
411
412 ?>