From 0fb5ad60b1104980f96073e25608729f0eb6eaf4 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Sun, 9 May 2004 02:32:04 +0000 Subject: [PATCH] Board of Trustees vote --- includes/DefaultSettings.php | 7 + includes/GlobalFunctions.php | 42 +++- includes/SpecialBoardvote.php | 347 ++++++++++++++++++++++++++++++++++ languages/Language.php | 38 +++- 4 files changed, 432 insertions(+), 2 deletions(-) create mode 100644 includes/SpecialBoardvote.php diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index 8e0b269f45..1490f9f529 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -369,4 +369,11 @@ $wgTidyOpts = ''; # See list of skins and their symbolic names in language/Language.php $wgDefaultSkin = "monobook"; +# Board of Trustees vote +$wgBoardVoteDB = "boardvote"; +$wgContributingCandidates = array(); +$wgVolunteerCandidates = array(); +$wgGPGCommand = "gpg"; +$wgGPGRecipient = "boardvote"; + ?> diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index fd823d0b3a..d526064bb0 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -148,6 +148,46 @@ function wfMungeToUtf8($string) { return $string; } +# Converts a single UTF-8 character into the corresponding HTML character entity +function wfUtf8Entity( $char ) { + # Find the length + $z = ord( $char{0} ); + if ( $z & 0x80 ) { + $length = 0; + while ( $z & 0x80 ) { + $length++; + $z <<= 1; + } + } else { + $length = 1; + } + + if ( $length != strlen( $char ) ) { + return ""; + } + if ( $length == 1 ) { + return $char; + } + + # Mask off the length-determining bits and shift back to the original location + $z &= 0xff; + $z >>= $length; + + # Add in the free bits from subsequent bytes + for ( $i=1; $i<$length; $i++ ) { + $z <<= 6; + $z |= ord( $char{$i} ) & 0x3f; + } + + # Make entity + return "&#$z;"; +} + +# Converts all multi-byte characters in a UTF-8 string into the appropriate character entity +function wfUtf8ToHTML($string) { + return preg_replace_callback( '/[\\xc0-\\xfd][\\x80-\\xbf]*/', 'wfUtf8Entity', $string ); +} + function wfDebug( $text, $logonly = false ) { global $wgOut, $wgDebugLogFile, $wgDebugComments, $wgProfileOnly; @@ -736,7 +776,7 @@ function wfEscapeShellArg( ) } if (substr(php_uname(), 0, 7) == "Windows") { - $retVal .= "\"$arg\""; + $retVal .= '"' . str_replace( '"','\"', $arg ) . '"'; } else { $retVal .= escapeshellarg( $arg ); } diff --git a/includes/SpecialBoardvote.php b/includes/SpecialBoardvote.php new file mode 100644 index 0000000000..1555eb4363 --- /dev/null +++ b/includes/SpecialBoardvote.php @@ -0,0 +1,347 @@ +mAction = $par; + } + + $form->execute(); +} + +class BoardVoteForm { + var $mPosted, $mContributing, $mVolunteer, $mDBname, $mUserDays, $mUserEdits; + var $mHasVoted, $mAction, $mUserKey; + + function BoardVoteForm( &$request, $dbName ) { + global $wgUser, $wgDBname, $wgInputEncoding; + + $this->mUserKey = iconv( $wgInputEncoding, "UTF-8", $wgUser->getName() ) . "@$wgDBname"; + $this->mPosted = $request->wasPosted(); + $this->mContributing = $request->getInt( "contributing" ); + $this->mVolunteer = $request->getInt( "volunteer" ); + $this->mDBname = $dbName; + $this->mHasVoted = $this->hasVoted( $wgUser ); + $this->mAction = $request->getText( "action" ); + } + + function execute() { + global $wgUser; + if ( $this->mAction == "list" ) { + $this->displayList(); + } elseif ( $this->mAction == "dump" ) { + $this->dump(); + } elseif( $this->mAction == "vote" ) { + if ( !$wgUser->getID() ) { + $this->notLoggedIn(); + } else { + $this->getQualifications( $wgUser ); + if ( $this->mUserDays < 90 ) { + $this->notQualified(); + } elseif ( $this->mPosted ) { + $this->logVote(); + } else { + $this->displayVote(); + } + } + } else { + $this->displayEntry(); + } + } + + function displayEntry() { + global $wgOut; + $wgOut->addWikiText( wfMsg( "boardvote_entry" ) ); + } + + function hasVoted( &$user ) { + global $wgDBname; + $row = wfGetArray( $this->mDBname . ".vote", array( "1" ), + array( "vote_key" => $this->mUserKey ), "BoardVoteForm::getUserVote" ); + if ( $row === false ) { + return false; + } else { + return true; + } + } + + function logVote() { + global $wgUser, $wgDBname, $wgIP, $wgOut; + $now = wfTimestampNow(); + $record = $this->encrypt( $this->mContributing, $this->mVolunteer ); + $db = $this->mDBname; + + # Add vote to log + wfInsertArray( "$db.log", array( + "log_user" => $wgUser->getID(), + "log_user_text" => $wgUser->getName(), + "log_user_key" => $this->mUserKey, + "log_wiki" => $wgDBname, + "log_edits" => $this->mUserEdits, + "log_days" => $this->mUserDays, + "log_record" => $record, + "log_ip" => $wgIP, + "log_xff" => @$_SERVER['HTTP_X_FORWARDED_FOR'], + "log_ua" => $_SERVER['HTTP_USER_AGENT'], + "log_timestamp" => $now + )); + + # Record vote in non-duplicating vote table + $sql = "REPLACE INTO $db.vote (vote_key,vote_record) " . + "VALUES ('". wfStrencode( $this->mUserKey ) . "','" . wfStrencode( $record ) . "')"; + wfQuery( $sql, DB_WRITE ); + + $wgOut->addWikiText( wfMsg( "boardvote_entered", $record ) ); + } + + function displayVote() { + global $wgContributingCandidates, $wgVolunteerCandidates, $wgOut; + + $thisTitle = Title::makeTitle( NS_SPECIAL, "Boardvote" ); + $action = $thisTitle->getLocalURL( "action=vote" ); + if ( $this->mHasVoted ) { + $intro = wfMsg( "boardvote_intro_change" ); + } else { + $intro = wfMsg( "boardvote_intro" ); + } + $contributing = wfMsg( "boardvote_contributing" ); + $volunteer = wfMsg( "boardvote_volunteer" ); + $ok = wfMsg( "ok" ); + + $candidatesV = $candidatesC = array(); + foreach( $wgContributingCandidates as $i => $candidate ) { + $candidatesC[] = array( $i, $candidate ); + } + foreach ( $wgVolunteerCandidates as $i => $candidate ) { + $candidatesV[] = array( $i, $candidate ); + } + + srand ((float)microtime()*1000000); + shuffle( $candidatesC ); + shuffle( $candidatesV ); + + $text = " + $intro +
+ "; + $text .= $this->voteEntry( -1, wfMsg( "boardvote_abstain" ), "contributing" ); + foreach ( $candidatesC as $candidate ) { + $text .= $this->voteEntry( $candidate[0], $candidate[1], "contributing" ); + } + $text .= " + "; + $text .= $this->voteEntry( -1, wfMsg( "boardvote_abstain" ), "volunteer" ); + foreach ( $candidatesV as $candidate ) { + $text .= $this->voteEntry( $candidate[0], $candidate[1], "volunteer" ); + } + + $text .= "
+

$contributing

+
+

$volunteer

  + +
"; + $text .= wfMsg( "boardvote_footer" ); + $wgOut->addHTML( $text ); + } + + function voteEntry( $index, $candidate, $name ) { + if ( $index == -1 ) { + $checked = " CHECKED"; + } else { + $checked = ""; + } + + return " + + + + $candidate + "; + } + + function notLoggedIn() { + global $wgOut; + $wgOut->addWikiText( wfMsg( "boardvote_notloggedin" ) ); + } + + function notQualified() { + global $wgOut; + $wgOut->addWikiText( wfMsg( "boardvote_notqualified", $this->mUserDays ) ); + } + + function encrypt( $contributing, $volunteer ) { + global $wgVolunteerCandidates, $wgContributingCandidates; + global $wgGPGCommand, $wgGPGRecipient; + $file = @fopen( "/dev/urandom", "r" ); + if ( $file ) { + $salt = implode( "", unpack( fread( $file, 64 ), "H*" )); + fclose( $file ); + } else { + $salt = Parser::getRandomString() . Parser::getRandomString(); + } + $record = + "Contributing: $contributing (" .$wgContributingCandidates[$contributing] . ")\n" . + "Volunteer: $volunteer (" . $wgVolunteerCandidates[$volunteer] . ")\n" . + "User: {$this->mUserKey}\n" . + "Salt: $salt\n"; + # Get file names + $input = tempnam( "/tmp", "gpg_" ); + $output = tempnam( "/tmp", "gpg_" ); + + # Write unencrypted record + $file = fopen( $input, "w" ); + fwrite( $file, $record ); + fclose( $file ); + + # Call GPG + $command = wfEscapeShellArg( $wgGPGCommand ) . " --batch --yes -ear " . + wfEscapeShellArg( $wgGPGRecipient ) . " -o " . wfEscapeShellArg( $output, $input ); + + shell_exec( $command ); + + # Read result + $result = file_get_contents( $output ); + + # Delete temporary files + #unlink( $input ); + #unlink( $output ); + + return $result; + } + + function getQualifications( &$user ) { + $id = $user->getID(); + if ( !$id ) { + $this->mUserDays = 0; + $this->mUserEdits = 0; + return; + } + + # Count contributions and find earliest edit + # First cur + $sql = "SELECT COUNT(*) as n, MIN(cur_timestamp) as t FROM cur WHERE cur_user=$id"; + $res = wfQuery( $sql, DB_READ, "BoardVoteForm::getQualifications" ); + $cur = wfFetchObject( $res ); + wfFreeResult( $res ); + + # If the user has stacks of contributions, don't check old as well + $signup = wfTimestamp2Unix( $cur->t ); + $now = time(); + $days = ($now - $signup) / 86400; + if ( $cur->n > 400 && $days > 180 ) { + $this->mUserDays = 0x7fffffff; + $this->mUserEdits = 0x7fffffff; + return; + } + + # Now check old + $sql = "SELECT COUNT(*) as n, MIN(old_timestamp) as t FROM old WHERE old_user=$id"; + $res = wfQuery( $sql, DB_READ, "BoardVoteForm::getQualifications" ); + $old = wfFetchObject( $res ); + wfFreeResult( $res ); + + $signup = min( wfTimestamp2Unix( $old->t ), $signup ); + $this->mUserDays = (int)(($now - $signup) / 86400); + $this->mUserEdits = $cur->n + $old->n; + } + + function displayList() { + global $wgOut, $wgOutputEncoding, $wgLang, $wgUser; + $sql = "SELECT log_timestamp,log_user_key FROM {$this->mDBname}.log"; + $res = wfQuery( $sql, DB_READ, "BoardVoteForm::list" ); + if ( wfNumRows( $res ) == 0 ) { + $wgOut->addWikiText( wfMsg( "boardvote_novotes" ) ); + return; + } + $thisTitle = Title::makeTitle( NS_SPECIAL, "Boardvote" ); + $sk = $wgUser->getSkin(); + $dumpLink = $sk->makeKnownLinkObj( $thisTitle, wfMsg( "boardvote_dumplink" ), "action=dump" ); + + $intro = wfMsg( "boardvote_listintro", $dumpLink ); + $hTime = wfMsg( "boardvote_time" ); + $hUser = wfMsg( "boardvote_user" ); + $hContributing = wfMsg( "boardvote_contributing" ); + $hVolunteer = wfMsg( "boardvote_volunteer" ); + + $s = "$intro "; + + while ( $row = wfFetchObject( $res ) ) { + if ( $wgOutputEncoding != "utf-8" ) { + $user = wfUtf8ToHTML( $row->log_user_key ); + } else { + $user = $row->log_user_key; + } + $time = $wgLang->timeanddate( $row->log_timestamp ); + $s .= ""; + } + $s .= "
+ $hTime + + $hUser +
+ $time + + $user +
"; + $wgOut->addHTML( $s ); + } + + function dump() { + global $wgOut, $wgOutputEncoding, $wgLang, $wgUser; + + $userRights = $wgUser->getRights(); + if ( in_array( "boardvote", $userRights ) ) { + $admin = true; + } else { + $admin = false; + } + + $sql = "SELECT * FROM {$this->mDBname}.log"; + $res = wfQuery( $sql, DB_READ, "BoardVoteForm::list" ); + if ( wfNumRows( $res ) == 0 ) { + $wgOut->addWikiText( wfMsg( "boardvote_novotes" ) ); + return; + } + $s = ""; + if ( $admin ) { + $s .= wfMsg( "boardvote_dumpheader" ); + } + + while ( $row = wfFetchObject( $res ) ) { + if ( $wgOutputEncoding != "utf-8" ) { + $user = wfUtf8ToHTML( $row->log_user_key ); + } else { + $user = $row->log_user_key; + } + $time = $wgLang->timeanddate( $row->log_timestamp ); + $record = nl2br( $row->log_record ); + if ( $admin ) { + $edits = $row->log_edits == 0x7fffffff ? "many" : $row->log_edits; + $days = $row->log_days == 0x7fffffff ? "many" : $row->log_days; + $s .= ""; + } else { + $s .= ""; + } + } + $s .= "
+ $time + + $user + + $edits + + $days + + {$row->log_ip} + + {$row->log_ua} + + $record +
$time$user$record
"; + $wgOut->addHTML( $s ); + } +} +?> diff --git a/languages/Language.php b/languages/Language.php index b51b08fc88..4520ec808f 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -422,7 +422,8 @@ $wgLanguageNamesEn =& $wgLanguageNames; "Export" => "XML page export", "Version" => "Show MediaWiki version", "Allmessages" => "All system messages", - "Search" => "", + "Search" => "" +# "Boardvote" => "Wikimedia Foundation Board of Trustees election" ); /* private */ $wgSysopSpecialPagesEn = array( @@ -1590,6 +1591,41 @@ amusement.", "and" => "and", "othercontribs" => "Based on work by $1.", "siteusers" => "$wgSitename user(s) $1", + +# Board vote +"boardvote" => "Wikimedia Board of Trustees election", +"boardvote_entry" => +"* [[Special:Boardvote/vote|Vote]] +* [[Special:Boardvote/list|List votes to date]] +* [[Special:Boardvote/dump|Dump encrypted election record]]", +"boardvote_intro" => "

Please choose your preferred candidate for both the +Contributing Representative and the Volunteer Representative.

", +"boardvote_intro_change" => "

You have voted before. However you may change +your vote using the form below.

", +"boardvote_abstain" => "Abstain", +"boardvote_footer" => " ", +"boardvote_entered" => "Thank you, your vote has been recorded. + +Following is your encrypted vote record. It will appear publicly at [[Special:Boardvote/dump]]. + +
$1
+ +[[Special:Boardvote/entry|Back]]", +"boardvote_notloggedin" => "You are not logged in. To vote, you must use an account +which has existed for at least 90 days.", +"boardvote_notqualified" => "Sorry, your first contribution was only $1 days ago. +You need to have been contributing for at least 90 days to vote in this election.", +"boardvote_novotes" => "Nobody has voted yet.", +"boardvote_contributing" => "Contributing candidate", +"boardvote_volunteer" => "Volunteer candidate", +"boardvote_time" => "Time", +"boardvote_user" => "User", +"boardvote_listintro" => "

This is a list of all votes which have been recorded +to date. $1 for the full encrypted election record.

", +"boardvote_dumplink" => "Click here", +"boardvote_dumpheader" => "Election administrator private dump +TimeUserEditsDays +IPUser agentRecord" ); #-------------------------------------------------------------------------- -- 2.20.1