http://www.mediawiki.org/wiki/Special:Code/MediaWiki/97068

Revision: 97068
Author:   ashley
Date:     2011-09-14 15:26:09 +0000 (Wed, 14 Sep 2011)
Log Message:
-----------
phase II social tools: VoteNY. Tested and built against MW 1.16.0. Can be used 
without the core social tools package (SocialProfile extension).

Added Paths:
-----------
    trunk/extensions/VoteNY/
    trunk/extensions/VoteNY/SpecialTopRatings.php
    trunk/extensions/VoteNY/Vote.css
    trunk/extensions/VoteNY/Vote.i18n.php
    trunk/extensions/VoteNY/Vote.js
    trunk/extensions/VoteNY/Vote.php
    trunk/extensions/VoteNY/VoteClass.php
    trunk/extensions/VoteNY/VoteHooks.php
    trunk/extensions/VoteNY/Vote_AjaxFunctions.php
    trunk/extensions/VoteNY/images/
    trunk/extensions/VoteNY/images/star_half.gif
    trunk/extensions/VoteNY/images/star_off.gif
    trunk/extensions/VoteNY/images/star_on.gif
    trunk/extensions/VoteNY/images/star_voted.gif
    trunk/extensions/VoteNY/vote.sql

Added: trunk/extensions/VoteNY/SpecialTopRatings.php
===================================================================
--- trunk/extensions/VoteNY/SpecialTopRatings.php                               
(rev 0)
+++ trunk/extensions/VoteNY/SpecialTopRatings.php       2011-09-14 15:26:09 UTC 
(rev 97068)
@@ -0,0 +1,185 @@
+<?php
+/**
+ * A special page to display the highest rated pages on the wiki.
+ *
+ * This special page supports filtering by category and namespace, so
+ * {{Special:TopRatings/Adventure Games/0/10}} will show 10 ratings where the
+ * pages are in the "Adventure Games" category and the pages are in the main
+ * (0) namespace.
+ *
+ * @file
+ * @ingroup Extensions
+ * @date 21 August 2011
+ * @license To the extent that it is possible, this code is in the public 
domain
+ */
+class SpecialTopRatings extends IncludableSpecialPage {
+
+       /**
+        * Constructor -- set up the new special page
+        */
+       public function __construct() {
+               parent::__construct( 'TopRatings' );
+       }
+
+       /**
+        * Show the special page
+        *
+        * @param $par Mixed: parameter passed to the special page or null
+        */
+       public function execute( $par ) {
+               global $wgOut, $wgScriptPath, $wgUser;
+
+               // Set the page title, robot policies, etc.
+               $this->setHeaders();
+
+               $categoryName = $namespace = '';
+
+               // Parse the parameters passed to the special page
+               // Make sure that the limit parameter passed to the special 
page is
+               // an integer and that it's less than 100 (performance!)
+               if ( isset( $par ) && is_numeric( $par ) && $par < 100 ) {
+                       $limit = intval( $par );
+               } elseif ( isset( $par ) && !is_numeric( $par ) ) {
+                       // $par is a string...assume that we can explode() it
+                       $exploded = explode( '/', $par );
+                       $categoryName = $exploded[0];
+                       $namespace = ( isset( $exploded[1] ) ? intval( 
$exploded[1] ) : $namespace );
+                       $limit = ( isset( $exploded[2] ) ? intval( $exploded[2] 
) : 50 );
+               } else {
+                       $limit = 50;
+               }
+
+               // Add JS -- needed so that users can vote on this page and so 
that
+               // their browsers' consoles won't be filled with JS errors ;-)
+               $wgOut->addScriptFile( $wgScriptPath . 
'/extensions/VoteNY/Vote.js' );
+
+               $ratings = array();
+               $output = '';
+               $sk = $wgUser->getSkin();
+
+               $dbr = wfGetDB( DB_SLAVE );
+               $tables = $where = $joinConds = array();
+               $whatToSelect = array( 'DISTINCT vote_page_id' );
+
+               // By default we have no category and no namespace
+               $tables = array( 'Vote' );
+               $where = array( 'vote_page_id <> 0' );
+
+               // isset(), because 0 is a totally valid NS
+               if ( !empty( $categoryName ) && isset( $namespace ) ) {
+                       $tables = array( 'Vote', 'page', 'categorylinks' );
+                       $where = array(
+                               'vote_page_id <> 0',
+                               'cl_to' => str_replace( ' ', '_', $categoryName 
),
+                               'page_namespace' => $namespace
+                       );
+                       $joinConds = array(
+                               'categorylinks' => array( 'INNER JOIN', 
'cl_from = page_id' ),
+                               'page' => array( 'INNER JOIN', 'page_id = 
vote_page_id' )
+                       );
+               }
+
+               // Perform the SQL query with the given conditions; the basic 
idea is
+               // that we get $limit (however, 100 or less) unique page IDs 
from the
+               // Vote table. If a category and a namespace have been given, 
we also
+               // do an INNER JOIN with page and categorylinks table to get the
+               // correct data.
+               $res = $dbr->select(
+                       $tables,
+                       $whatToSelect,
+                       $where,
+                       __METHOD__,
+                       array( 'LIMIT' => intval( $limit ) ),
+                       $joinConds
+               );
+
+               foreach ( $res as $row ) {
+                       // Add the results to the $ratings array and get the 
amount of
+                       // votes the given page ID has
+                       // For example: $ratings[1] = 11 = page with the page 
ID 1 has 11
+                       // votes
+                       $ratings[$row->vote_page_id] = (int)$dbr->selectField(
+                               'Vote',
+                               'SUM(vote_value)',
+                               array( 'vote_page_id' => $row->vote_page_id ),
+                               __METHOD__
+                       );
+               }
+
+               // If we have some ratings, start building HTML output
+               if ( !empty( $ratings ) ) {
+                       /* XXX dirrrrrrty hack! because when we include this 
page, the JS
+                       is not included, but we want things to work still */
+                       if ( $this->including() ) {
+                               $output .= '<script type="text/javascript" 
src="' .
+                                       $wgScriptPath . 
'/extensions/VoteNY/Vote.js"></script>';
+                       }
+
+                       // yes, array_keys() is needed
+                       foreach ( array_keys( $ratings ) as $discardThis => 
$pageId ) {
+                               $titleObj = Title::newFromId( $pageId );
+                               if ( !( $titleObj instanceof Title ) ) {
+                                       continue;
+                               }
+
+                               $vote = new VoteStars( $pageId );
+                               $output .= '<div class="user-list-rating">' .
+                                       $sk->link(
+                                               $titleObj,
+                                               $titleObj->getPrefixedText() // 
prefixed, so that the namespace shows!
+                                       ) . wfMsg( 'word-separator' ) . // i18n 
overkill? ya betcha...
+                                       wfMsg( 'parentheses', $ratings[$pageId] 
) .
+                               '</div>';
+
+                               $id = mt_rand(); // AFAIK these IDs are and 
originally were totally random...
+                               $output .= "<div id=\"rating_stars_{$id}\">" .
+                                       $vote->displayStars(
+                                               $id,
+                                               self::getAverageRatingForPage( 
$pageId ),
+                                               false
+                                       ) . '</div>';
+                               $output .= "<div id=\"rating_{$id}\" 
class=\"rating-total\">" .
+                                       $vote->displayScore() .
+                               '</div>';
+                       }
+               } else {
+                       // Nothing? Well, display an informative error message 
rather than
+                       // a blank page or somesuch.
+                       $output .= wfMsg( 'topratings-no-pages' );
+               }
+
+               // Output everything!
+               $wgOut->addHTML( $output );
+       }
+
+       /**
+        * Static version of Vote::getAverageVote().
+        *
+        * @param $pageId Integer: ID of the page for which we want to get the 
avg.
+        *                         rating
+        * @return Integer: average vote for the given page (ID)
+        */
+       public static function getAverageRatingForPage( $pageId ) {
+               global $wgMemc;
+
+               $key = wfMemcKey( 'vote', 'avg', $pageId );
+               $data = $wgMemc->get( $key );
+               $voteAvg = 0;
+
+               if( $data ) {
+                       wfDebug( "Loading vote avg for page {$pageId} from 
cache (TopRatings)\n" );
+                       $voteAvg = $data;
+               } else {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $voteAvg = (int)$dbr->selectField(
+                               'Vote',
+                               'AVG(vote_value) AS VoteAvg',
+                               array( 'vote_page_id' => $pageId ),
+                               __METHOD__
+                       );
+                       $wgMemc->set( $key, $voteAvg );
+               }
+
+               return $voteAvg;
+       }
+}
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/SpecialTopRatings.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/Vote.css
===================================================================
--- trunk/extensions/VoteNY/Vote.css                            (rev 0)
+++ trunk/extensions/VoteNY/Vote.css    2011-09-14 15:26:09 UTC (rev 97068)
@@ -0,0 +1,63 @@
+/* CSS for Vote extension */
+.vote-box {
+       background-color: #68BD46;
+       height: 30px;
+       padding: 13px 0px 0px;
+       text-align: center;
+       width: 43px;
+}
+
+.vote-number {
+       color: #FFF;
+       font-size: 16px;
+       font-weight: bold;
+}
+
+.vote-action {
+       text-align: center;
+       width: 43px;
+}
+
+.vote-action a {
+       font-weight: bold;
+       font-size: 11px;
+       text-decoration: none;
+}
+
+.rating-score {
+       background-color: #68BD46;
+       color: #FFF;
+       float: left;
+       font-size: 14px;
+       font-weight: bold;
+       padding: 1px 8px 0px;
+       margin: 1px 7px 0px 0px;
+       text-align: center;
+}
+
+.ratings-top {
+       position: absolute;
+       top: 37px !important;
+       right: 0px !important;
+       width: 100%;
+}
+
+.rating-section img {
+       vertical-align: text-bottom;
+}
+
+.rating-voted {
+       color: #666666;
+       line-height: 10px;
+       font-size: 9px;
+       position: absolute;
+       right: 0px;
+}
+
+/* Styling for the (n votes) after rating box/stars */
+.rating-total {
+       color: #666;
+       font-weight: bold;
+       font-size: 11px;
+       margin: 3px 0px 0px 0px;
+}
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/Vote.css
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/Vote.i18n.php
===================================================================
--- trunk/extensions/VoteNY/Vote.i18n.php                               (rev 0)
+++ trunk/extensions/VoteNY/Vote.i18n.php       2011-09-14 15:26:09 UTC (rev 
97068)
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Internationalization file for the Vote extension.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+
+$messages = array();
+
+/** English
+ * @author Aaron Wright <aaron.wri...@gmail.com>
+ * @author David Pean <david.p...@gmail.com>
+ */
+$messages['en'] = array(
+       'vote-link' => 'Vote',
+       'vote-unvote-link' => 'unvote',
+       'vote-community-score' => 'community score: $1',
+       'vote-ratings' => '{{PLURAL:$1|one rating|$1 ratings}}',
+       'vote-remove' => 'remove',
+       'vote-gave-this' => 'you gave this a $1',
+       'vote-votes' => '{{PLURAL:$1|one vote|$1 votes}}',
+       // Special:TopRatings
+       'topratings' => 'Top rated pages',
+       'topratings-no-pages' => 'No top rated pages.',
+       // For Special:ListGroupRights
+       'right-vote' => 'Vote pages',
+);
+
+/** Finnish (Suomi)
+ * @author Jack Phoenix <j...@countervandalism.net>
+ */
+$messages['fi'] = array(
+       'vote-link' => 'Äänestä',
+       'vote-unvote-link' => 'poista ääni',
+       'vote-community-score' => 'yhteisön antama pistemäärä: $1',
+       'vote-ratings' => '{{PLURAL:$1|yksi arvostelu|$1 arvostelua}}',
+       'vote-remove' => 'poista',
+       'vote-gave-this' => 'annoit tälle {{PLURAL:$1|yhden tähden|$1 tähteä}}',
+       'vote-votes' => '{{PLURAL:$1|yksi ääni|$1 ääntä}}',
+       'topratings' => 'Huippusivut',
+       'topratings-no-pages' => 'Ei huippusivuja.',
+       'right-vote' => 'Äänestää sivuja',
+);
+
+/** French (Français)
+ * @author Jack Phoenix <j...@countervandalism.net>
+ */
+$messages['fr'] = array(
+       'vote-link' => 'Voter',
+       'vote-unvote-link' => 'supprimer vote',
+       'vote-remove' => 'supprimer',
+       'vote-votes' => '{{PLURAL:$1|un vote|$1 votes}}',
+       'right-vote' => 'Voter pages',
+);
+
+/** Dutch (Nederlands)
+ * @author Mitchel Corstjens
+ */
+$messages['nl'] = array(
+       'vote-link' => 'Stem',
+       'vote-unvote-link' => 'stem terugtrekken',
+       'vote-community-score' => 'gemeenschap score: $1',
+       'vote-remove' => 'verwijder',
+       'vote-gave-this' => 'je gaf dit een $1',
+       'vote-votes' => '{{PLURAL:$1|een stem|$1 stemmen}}',
+       'topratings' => 'Meest gewaardeerde pagina\'s',
+       'topratings-no-pages' => 'Er zijn nog geen meest gewaardeerde 
pagina\'s',
+       'right-vote' => 'Stem paginas',
+);
+
+/** Polish (Polski)
+ * @author Misiek95
+ */
+$messages['pl'] = array(
+       'vote-link' => 'Głosuj',
+       'vote-unvote-link' => 'Anuluj',
+       'vote-community-score' => 'Wynik wśród społeczności: $1',
+       'vote-ratings' => '{{PLURAL:$1|1 głos|$1 głosy|$1 głosów}}',
+       'vote-remove' => 'usuń',
+       'vote-gave-this' => 'Oceniłeś to na $1',
+       'vote-votes' => '{{PLURAL:$1|1 głos|$1 głosy|$1 głosów}}',
+       'right-vote' => 'Udział w głosowaniach',
+);
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/Vote.i18n.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/Vote.js
===================================================================
--- trunk/extensions/VoteNY/Vote.js                             (rev 0)
+++ trunk/extensions/VoteNY/Vote.js     2011-09-14 15:26:09 UTC (rev 97068)
@@ -0,0 +1,136 @@
+/**
+ * JavaScript functions for Vote extension
+ *
+ * @file
+ * @ingroup Extensions
+ * @author Jack Phoenix <j...@countervandalism.net>
+ * @date 19 June 2011
+ */
+var VoteNY = {
+       MaxRating: 5,
+       clearRatingTimer: '',
+       voted_new: [],
+       id: 0,
+       last_id: 0,
+       imagePath: wgScriptPath + '/extensions/VoteNY/images/',
+
+       /**
+        * Called when voting through the green square voting box
+        * @param TheVote
+        * @param PageID Integer: internal ID number of the current article
+        * @param mk Mixed: random token
+        */
+       clickVote: function( TheVote, PageID, mk ) {
+               sajax_request_type = 'POST';
+               sajax_do_call( 'wfVoteClick', [ TheVote, PageID, mk ], 
function( request ) {
+                       document.getElementById( 'votebox' ).style.cursor = 
'default';
+                       document.getElementById( 'PollVotes' ).innerHTML = 
request.responseText;
+                       var unvoteMessage;
+                       if ( typeof( mediaWiki ) == 'undefined' ) {
+                               unvoteMessage = _UNVOTE_LINK;
+                       } else {
+                               unvoteMessage = mediaWiki.msg( 
'vote-unvote-link' );
+                       }
+                       document.getElementById( 'Answer' ).innerHTML =
+                               "<a href=javascript:VoteNY.unVote(" + PageID + 
",'" + mk +
+                               "')>" + unvoteMessage + '</a>';
+               } );
+       },
+
+       /**
+        * Called when removing your vote through the green square voting box
+        * @param PageID Integer: internal ID number of the current article
+        * @param mk Mixed: random token
+        */
+       unVote: function( PageID, mk ) {
+               sajax_request_type = 'POST';
+               sajax_do_call( 'wfVoteDelete', [ PageID, mk ], function( 
request ) {
+                       document.getElementById( 'votebox' ).style.cursor = 
'pointer';
+                       document.getElementById( 'PollVotes' ).innerHTML = 
request.responseText;
+                       var voteMessage;
+                       if ( typeof( mediaWiki ) == 'undefined' ) {
+                               voteMessage = _VOTE_LINK;
+                       } else {
+                               voteMessage = mediaWiki.msg( 'vote-link' );
+                       }
+                       document.getElementById( 'Answer' ).innerHTML =
+                               '<a href=javascript:VoteNY.clickVote(1,' + 
PageID + ',"' + mk +
+                               '")>' + voteMessage + '</a>';
+               } );
+       },
+
+       /**
+        * Called when adding a vote after a user has clicked the yellow voting 
stars
+        * @param PageID Integer: internal ID number of the current article
+        * @param mk Mixed: random token
+        * @param id Integer: ID of the current rating star
+        * @param action Integer: controls which AJAX function will be called
+        */
+       clickVoteStars: function( TheVote, PageID, mk, id, action ) {
+               VoteNY.voted_new[id] = TheVote;
+               var rsfun;
+               if( action == 3 ) {
+                       rsfun = 'wfVoteStars';
+               }
+               if( action == 5 ) {
+                       rsfun = 'wfVoteStarsMulti';
+               }
+
+               var resultElement = document.getElementById( 'rating_' + id );
+               sajax_request_type = 'POST';
+               sajax_do_call( rsfun, [ TheVote, PageID, mk ], resultElement );
+       },
+
+       /**
+        * Called when removing your vote through the yellow voting stars
+        * @param PageID Integer: internal ID number of the current article
+        * @param mk Mixed: random token
+        * @param id Integer: ID of the current rating star
+        */
+       unVoteStars: function( PageID, mk, id ) {
+               var resultElement = document.getElementById( 'rating_' + id );
+               sajax_request_type = 'POST';
+               sajax_do_call( 'wfVoteStarsDelete', [ PageID, mk ], 
resultElement );
+       },
+
+       startClearRating: function( id, rating, voted ) {
+               VoteNY.clearRatingTimer = setTimeout(
+                       "VoteNY.clearRating('" + id + "',0," + rating + ',' + 
voted + ')',
+                       200
+               );
+       },
+
+       clearRating: function( id, num, prev_rating, voted ) {
+               if( VoteNY.voted_new[id] ) {
+                       voted = VoteNY.voted_new[id];
+               }
+
+               for( var x = 1; x <= VoteNY.MaxRating; x++ ) {
+                       var star_on, old_rating;
+                       if( voted ) {
+                               star_on = 'voted';
+                               old_rating = voted;
+                       } else {
+                               star_on = 'on';
+                               old_rating = prev_rating;
+                       }
+                       var ratingElement = document.getElementById( 'rating_' 
+ id + '_' + x );
+                       if( !num && old_rating >= x ) {
+                               ratingElement.src = VoteNY.imagePath + 'star_' 
+ star_on + '.gif';
+                       } else {
+                               ratingElement.src = VoteNY.imagePath + 
'star_off.gif';
+                       }
+               }
+       },
+
+       updateRating: function( id, num, prev_rating ) {
+               if( VoteNY.clearRatingTimer && VoteNY.last_id == id ) {
+                       clearTimeout( VoteNY.clearRatingTimer );
+               }
+               VoteNY.clearRating( id, num, prev_rating );
+               for( var x = 1; x <= num; x++ ) {
+                       document.getElementById( 'rating_' + id + '_' + x ).src 
= VoteNY.imagePath + 'star_voted.gif';
+               }
+               VoteNY.last_id = id;
+       }
+};
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/Vote.js
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/Vote.php
===================================================================
--- trunk/extensions/VoteNY/Vote.php                            (rev 0)
+++ trunk/extensions/VoteNY/Vote.php    2011-09-14 15:26:09 UTC (rev 97068)
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Vote extension - JavaScript-based voting with the <vote> tag
+ *
+ * @file
+ * @ingroup Extensions
+ * @version 2.3.3
+ * @author Aaron Wright <aaron.wri...@gmail.com>
+ * @author David Pean <david.p...@gmail.com>
+ * @author Jack Phoenix <j...@countervandalism.net>
+ * @link http://www.mediawiki.org/wiki/Extension:VoteNY Documentation
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 
2.0 or later
+ */
+
+/**
+ * Protect against register_globals vulnerabilities.
+ * This line must be present before any global variable is referenced.
+ */
+if ( !defined( 'MEDIAWIKI' ) ) {
+       die( "This is not a valid entry point.\n" );
+}
+
+// Extension credits that show up on Special:Version
+$wgExtensionCredits['parserhook'][] = array(
+       'name' => 'Vote',
+       'version' => '2.3.3',
+       'author' => array( 'Aaron Wright', 'David Pean', 'Jack Phoenix' ),
+       'description' => 'JavaScript-based voting with the 
<tt>&lt;vote&gt;</tt> tag',
+       'url' => 'http://www.mediawiki.org/wiki/Extension:VoteNY'
+);
+
+// Path to Vote extension files
+$wgVoteDirectory = "$IP/extensions/VoteNY";
+
+// New user right
+$wgAvailableRights[] = 'vote';
+$wgGroupPermissions['*']['vote'] = false; // Anonymous users cannot vote
+$wgGroupPermissions['user']['vote'] = true; // Registered users can vote
+
+// AJAX functions needed by this extension
+require_once( 'Vote_AjaxFunctions.php' );
+
+// Autoload classes and set up i18n
+$dir = dirname( __FILE__ ) . '/';
+$wgExtensionMessagesFiles['Vote'] = $dir . 'Vote.i18n.php';
+$wgAutoloadClasses['Vote'] = $dir . 'VoteClass.php';
+$wgAutoloadClasses['VoteStars'] = $dir . 'VoteClass.php';
+
+// Set up the new special page, Special:TopRatings, which shows top rated pages
+// based on given criteria
+$wgAutoloadClasses['SpecialTopRatings'] = $dir . 'SpecialTopRatings.php';
+$wgSpecialPages['TopRatings'] = 'SpecialTopRatings';
+
+// Hooked functions
+$wgAutoloadClasses['VoteHooks'] = $dir . 'VoteHooks.php';
+
+$wgHooks['ParserFirstCallInit'][] = 'VoteHooks::registerParserHook';
+$wgHooks['MakeGlobalVariablesScript'][] = 'VoteHooks::addJSGlobalVariables';
+$wgHooks['RenameUserSQL'][] = 'VoteHooks::onUserRename';
+// Translations for {{NUMBEROFVOTES}}
+//$wgExtensionMessagesFiles['NumberOfVotes'] = $dir . 'Vote.i18n.magic.php';
+$wgHooks['LanguageGetMagic'][] = 'VoteHooks::setUpMagicWord';
+$wgHooks['ParserGetVariableValueSwitch'][] = 
'VoteHooks::assignValueToMagicWord';
+$wgHooks['MagicWordwgVariableIDs'][] = 'VoteHooks::registerVariableId';
+$wgHooks['LoadExtensionSchemaUpdates'][] = 'VoteHooks::addTable';
+
+// ResourceLoader support for MediaWiki 1.17+
+$wgResourceModules['ext.voteNY'] = array(
+       'styles' => 'Vote.css',
+       'scripts' => 'Vote.js',
+       'messages' => array( 'vote-link', 'vote-unvote-link' ),
+       'localBasePath' => dirname( __FILE__ ),
+       'remoteExtPath' => 'VoteNY',
+       'position' => 'top' // available since r85616
+);
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/Vote.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/VoteClass.php
===================================================================
--- trunk/extensions/VoteNY/VoteClass.php                               (rev 0)
+++ trunk/extensions/VoteNY/VoteClass.php       2011-09-14 15:26:09 UTC (rev 
97068)
@@ -0,0 +1,355 @@
+<?php
+/**
+ * Vote class - class for handling vote-related functions (counting
+ * the average score of a given page, inserting/updating/removing a vote etc.)
+ *
+ * @file
+ * @ingroup Extensions
+ */
+class Vote {
+       var $PageID = 0;
+       var $Userid = 0;
+       var $Username = null;
+
+       /**
+        * Constructor
+        * @param $pageID Integer: article ID number
+        */
+       public function __construct( $pageID ) {
+               global $wgUser;
+
+               $this->PageID = $pageID;
+               $this->Username = $wgUser->getName();
+               $this->Userid = $wgUser->getID();
+       }
+
+       /**
+        * Counts all votes, fetching the data from memcached if available
+        * or from the database if memcached isn't available
+        * @return Integer: amount of votes
+        */
+       function count() {
+               global $wgMemc;
+               $key = wfMemcKey( 'vote', 'count', $this->PageID );
+               $data = $wgMemc->get( $key );
+
+               // Try cache
+               if( $data ) {
+                       wfDebug( "Loading vote count for page {$this->PageID} 
from cache\n" );
+                       $vote_count = $data;
+               } else {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $vote_count = 0;
+                       $res = $dbr->select(
+                               'Vote',
+                               'COUNT(*) AS VoteCount',
+                               array( 'vote_page_id' => $this->PageID ),
+                               __METHOD__
+                       );
+                       $row = $dbr->fetchObject( $res );
+                       if( $row ) {
+                               $vote_count = $row->VoteCount;
+                       }
+                       $wgMemc->set( $key, $vote_count );
+               }
+               return $vote_count;
+       }
+
+       /**
+        * Gets the average score of all votes
+        * @return Integer: formatted average number of votes (something like 
3.50)
+        */
+       function getAverageVote() {
+               global $wgMemc;
+               $key = wfMemcKey( 'vote', 'avg', $this->PageID );
+               $data = $wgMemc->get( $key );
+
+               $voteAvg = 0;
+               if( $data ) {
+                       wfDebug( "Loading vote avg for page {$this->PageID} 
from cache\n" );
+                       $voteAvg = $data;
+               } else {
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $res = $dbr->select(
+                               'Vote',
+                               'AVG(vote_value) AS VoteAvg',
+                               array( 'vote_page_id' => $this->PageID ),
+                               __METHOD__
+                       );
+                       $row = $dbr->fetchObject( $res );
+                       if( $row ) {
+                               $voteAvg = $row->VoteAvg;
+                       }
+                       $wgMemc->set( $key, $voteAvg );
+               }
+               return number_format( $voteAvg, 2 );
+       }
+
+       /**
+        * Clear caches - memcached, parser cache and Squid cache
+        */
+       function clearCache() {
+               global $wgUser, $wgMemc;
+
+               // Kill internal cache
+               $wgMemc->delete( wfMemcKey( 'vote', 'count', $this->PageID ) );
+               $wgMemc->delete( wfMemcKey( 'vote', 'avg', $this->PageID ) );
+
+               // Purge squid
+               $page_title = Title::newFromID( $this->PageID );
+               if( is_object( $page_title ) ) {
+                       $page_title->invalidateCache();
+                       $page_title->purgeSquid();
+
+                       // Kill parser cache
+                       $article = new Article( $page_title );
+                       $parserCache =& ParserCache::singleton();
+                       $parser_key = $parserCache->getKey( $article, $wgUser );
+                       $wgMemc->delete( $parser_key );
+               }
+       }
+
+       /**
+        * Delete the user's vote from the DB if s/he wants to remove his/her 
vote
+        */
+       function delete() {
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->delete(
+                       'Vote',
+                       array(
+                               'vote_page_id' => $this->PageID,
+                               'username' => $this->Username
+                       ),
+                       __METHOD__
+               );
+               $dbw->commit();
+
+               $this->clearCache();
+
+               // Update social statistics if SocialProfile extension is 
enabled
+               if( class_exists( 'UserStatsTrack' ) ) {
+                       $stats = new UserStatsTrack( $this->Userid, 
$this->Username );
+                       $stats->decStatField( 'vote' );
+               }
+       }
+
+       /**
+        * Inserts a new vote into the Vote database table
+        * @param $voteValue
+        */
+       function insert( $voteValue ) {
+               $dbw = wfGetDB( DB_MASTER );
+               wfSuppressWarnings(); // E_STRICT whining
+               $voteDate = date( 'Y-m-d H:i:s' );
+               wfRestoreWarnings();
+               if( $this->UserAlreadyVoted() == false ) {
+                       $dbw->insert(
+                               'Vote',
+                               array(
+                                       'username' => $this->Username,
+                                       'vote_user_id' => $this->Userid,
+                                       'vote_page_id' => $this->PageID,
+                                       'vote_value' => $voteValue,
+                                       'vote_date' => $voteDate,
+                                       'vote_ip' => wfGetIP()
+                               ),
+                               __METHOD__
+                       );
+                       $dbw->commit();
+
+                       $this->clearCache();
+
+                       // Update social statistics if SocialProfile extension 
is enabled
+                       if( class_exists( 'UserStatsTrack' ) ) {
+                               $stats = new UserStatsTrack( $this->Userid, 
$this->Username );
+                               $stats->incStatField( 'vote' );
+                       }
+               }
+       }
+
+       /**
+        * Checks if a user has already voted
+        * @return Boolean: false if s/he hasn't, otherwise returns the value of
+        *                  'vote_value' column from Vote DB table
+        */
+       function UserAlreadyVoted() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $s = $dbr->selectRow(
+                       'Vote',
+                       array( 'vote_value' ),
+                       array(
+                               'vote_page_id' => $this->PageID,
+                               'username' => $this->Username
+                       ),
+                       __METHOD__
+               );
+               if( $s === false ) {
+                       return false;
+               } else {
+                       return $s->vote_value;
+               }
+       }
+
+       /**
+        * Displays the green voting box
+        * @return Mixed: HTML output
+        */
+       function display() {
+               global $wgUser;
+
+               $this->votekey = md5( $this->PageID . 'pants' . $this->Username 
);
+               $voted = $this->UserAlreadyVoted();
+
+               $make_vote_box_clickable = '';
+               if( $voted == false ) {
+                       $make_vote_box_clickable = ' vote-clickable';
+               }
+
+               $output = "<div class=\"vote-box{$make_vote_box_clickable}\" 
id=\"votebox\" 
onclick=\"VoteNY.clickVote(1,{$this->PageID},'{$this->votekey}')\">";
+               $output .= '<span id="PollVotes" class="vote-number">' . 
$this->count() . '</span>';
+               $output .= '</div>';
+               $output .= '<div id="Answer" class="vote-action">';
+
+               if ( !$wgUser->isAllowed( 'vote' ) ) {
+                       // @todo FIXME: this is horrible. If we don't have 
enough
+                       // permissions to vote, we should tell the end-user 
/that/,
+                       // not require them to log in!
+                       $login = SpecialPage::getTitleFor( 'Userlogin' );
+                       $output .= '<a class="votebutton" href="' .
+                               $login->escapeFullURL() . '" rel="nofollow">' .
+                               wfMsg( 'vote-link' ) . '</a>';
+               } else {
+                       if( !wfReadOnly() ) {
+                               if( $voted == false ) {
+                                       $output .= "<a 
href=\"javascript:VoteNY.clickVote(1,{$this->PageID},'{$this->votekey}')\">" .
+                                               wfMsg( 'vote-link' ) . '</a>';
+                               } else {
+                                       $output .= "<a 
href=\"javascript:VoteNY.unVote('{$this->PageID}', '{$this->votekey}')\">" .
+                                               wfMsg( 'vote-unvote-link' ) . 
'</a>';
+                               }
+                       }
+               }
+               $output .= '</div>';
+
+               return $output;
+       }
+}
+
+/**
+ * Class for generating star rating stars.
+ */
+class VoteStars extends Vote {
+
+       var $maxRating = 5;
+
+       /**
+        * Displays voting stars
+        * @param $voted Boolean: false by default
+        * @return Mixed: HTML output
+        */
+       function display( $voted = false ) {
+               global $wgUser;
+
+               $overall_rating = $this->getAverageVote();
+
+               if( $voted ) {
+                       $display_stars_rating = $voted;
+               } else {
+                       $display_stars_rating = $this->getAverageVote();
+               }
+
+               $this->votekey = md5( $this->PageID . 'pants' . $this->Username 
);
+               $id = '';
+
+               // Should probably be $this->PageID or something?
+               // 'cause we define $id just above as an empty string...duh
+               $output = '<div id="rating_' . $id . '">';
+               $output .= '<div class="rating-score">';
+               $output .= '<div class="voteboxrate">' . $overall_rating . 
'</div>';
+               $output .= '</div>';
+               $output .= '<div class="rating-section">';
+               $output .= $this->displayStars( $id, $display_stars_rating, 
$voted );
+               $count = $this->count();
+               if( $count ) {
+                       $output .= ' <span class="rating-total">(' .
+                               wfMsgExt( 'vote-votes', 'parsemag', $count ) . 
')</span>';
+               }
+               $already_voted = $this->UserAlreadyVoted();
+               if( $already_voted && $wgUser->isLoggedIn() ) {
+                       $output .= '<div class="rating-voted">' .
+                               wfMsgExt( 'vote-gave-this', 'parsemag', 
$already_voted ) .
+                       " </div>
+                       <a 
href=\"javascript:VoteNY.unVoteStars({$this->PageID},'{$this->votekey}','{$id}')\">("
+                               . wfMsg( 'vote-remove' ) .
+                       ')</a>';
+               }
+               $output .= '</div>
+                               <div class="rating-clear">
+                       </div>';
+
+               $output .= '</div>';
+               return $output;
+       }
+
+       /**
+        * Displays the actual star images, depending on the state of the 
user's mouse
+        * @param $id Integer: ID of the rating (div) element
+        * @param $rating Integer: average rating
+        * @param $voted Integer
+        * @return Mixed: generated <img> tag
+        */
+       function displayStars( $id, $rating, $voted ) {
+               global $wgScriptPath;
+
+               if( !$rating ) {
+                       $rating = 0;
+               }
+               $this->votekey = md5( $this->PageID . 'pants' . $this->Username 
);
+               if( !$voted ) {
+                       $voted = 0;
+               }
+               $output = '';
+
+               for( $x = 1; $x <= $this->maxRating; $x++ ) {
+                       if( !$id ) {
+                               $action = 3;
+                       } else {
+                               $action = 5;
+                       }
+                       $onclick = 
"VoteNY.clickVoteStars({$x},{$this->PageID},'{$this->votekey}','{$id}',$action);";
+                       $onmouseover = 
"VoteNY.updateRating('{$id}',{$x},{$rating});";
+                       $onmouseout = 
"VoteNY.startClearRating('{$id}','{$rating}',{$voted});";
+                       $output .= "<img onclick=\"javascript:{$onclick}\" 
onmouseover=\"javascript:{$onmouseover}\" 
onmouseout=\"javascript:{$onmouseout}\" id=\"rating_{$id}_{$x}\" 
src=\"{$wgScriptPath}/extensions/VoteNY/images/star_";
+                       switch( true ) {
+                               case $rating >= $x:
+                                       if( $voted ) {
+                                               $output .= 'voted';
+                                       } else {
+                                               $output .= 'on';
+                                       }
+                                       break;
+                               case( $rating > 0 && $rating < $x && $rating > 
( $x - 1 ) ):
+                                       $output .= 'half';
+                                       break;
+                               case( $rating < $x ):
+                                       $output .= 'off';
+                                       break;
+                       }
+
+                       $output .= '.gif" alt="" />';
+               }
+
+               return $output;
+       }
+
+       /**
+        * Displays the average score for the current page
+        * and the total amount of votes.
+        */
+       function displayScore() {
+               $count = $this->count();
+               return wfMsg( 'vote-community-score', '<b>' . 
$this->getAverageVote() . '</b>' ) .
+                               ' (' . wfMsgExt( 'vote-ratings', 'parsemag', 
$count ) . ')';
+       }
+
+}
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/VoteClass.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/VoteHooks.php
===================================================================
--- trunk/extensions/VoteNY/VoteHooks.php                               (rev 0)
+++ trunk/extensions/VoteNY/VoteHooks.php       2011-09-14 15:26:09 UTC (rev 
97068)
@@ -0,0 +1,189 @@
+<?php
+/**
+ * All hooked functions used by VoteNY extension.
+ *
+ * @file
+ * @ingroup Extensions
+ */
+class VoteHooks {
+
+       /**
+        * Set up the <vote> parser hook.
+        *
+        * @param $parser Parser: instance of Parser
+        * @return Boolean: true
+        */
+       public static function registerParserHook( &$parser ) {
+               $parser->setHook( 'vote', array( 'VoteHooks', 'renderVote' ) );
+               return true;
+       }
+
+       /**
+        * Callback function for registerParserHook.
+        *
+        * @param $input String: user-supplied input, unused
+        * @param $args Array: user-supplied arguments, unused
+        * @param $parser Parser: instance of Parser, unused
+        * @return String: HTML
+        */
+       public static function renderVote( $input, $args, $parser ) {
+               global $wgOut, $wgTitle, $wgScriptPath;
+
+               wfProfileIn( __METHOD__ );
+
+               // Disable parser cache (sadly we have to do this, because the 
caching is
+               // messing stuff up; we want to show an up-to-date rating 
instead of old
+               // or totally wrong rating, i.e. another page's rating...)
+               $parser->disableCache();
+
+               // Add CSS & JS
+               // In order for us to do this *here* instead of having to do 
this in
+               // registerParserHook(), we must've disabled parser cache
+               if ( defined( 'MW_SUPPORTS_RESOURCE_MODULES' ) ) {
+                       $wgOut->addModules( 'ext.voteNY' );
+               } else {
+                       $wgOut->addScriptFile( $wgScriptPath . 
'/extensions/VoteNY/Vote.js' );
+                       $wgOut->addExtensionStyle( $wgScriptPath . 
'/extensions/VoteNY/Vote.css' );
+               }
+
+               // Define variable - 0 means that we'll get that green voting 
box by default
+               $type = 0;
+
+               // Determine what kind of a voting gadget the user wants: a box 
or pretty stars?
+               if(     preg_match( "/^\s*type\s*=\s*(.*)/mi", $input, $matches 
) ) {
+                       $type = htmlspecialchars( $matches[1] );
+               } elseif( !empty( $args['type'] ) ) {
+                       $type = intval( $args['type'] );
+               }
+
+               $articleID = $wgTitle->getArticleID();
+               switch( $type ) {
+                       case 0:
+                               $vote = new Vote( $articleID );
+                               break;
+                       case 1:
+                               $vote = new VoteStars( $articleID );
+                               break;
+                       default:
+                               $vote = new Vote( $articleID );
+               }
+
+               $output = $vote->display();
+
+               wfProfileOut( __METHOD__ );
+
+               return $output;
+       }
+
+       /**
+        * Adds required JS variables to the HTML output.
+        *
+        * @param $vars Array: array of pre-existing JS globals
+        * @return Boolean: true
+        */
+       public static function addJSGlobalVariables( $vars ) {
+               $vars['_VOTE_LINK'] = wfMsg( 'vote-link' );
+               $vars['_UNVOTE_LINK'] = wfMsg( 'vote-unvote-link' );
+               return true;
+       }
+
+       /**
+        * For the Renameuser extension.
+        *
+        * @param $renameUserSQL
+        * @return Boolean: true
+        */
+       public static function onUserRename( $renameUserSQL ) {
+               $renameUserSQL->tables['Vote'] = array( 'username', 
'vote_user_id' );
+               return true;
+       }
+
+       /**
+        * Set up the {{NUMBEROFVOTES}} magic word.
+        *
+        * @param $magicWords Array: array of magic words
+        * @param $langID
+        * @return Boolean: true
+        */
+       public static function setUpMagicWord( &$magicWords, $langID ) {
+               // tell MediaWiki that {{NUMBEROFVOTES}} and all case variants 
found in
+               // wiki text should be mapped to magic ID 'NUMBEROFVOTES'
+               // (0 means case-insensitive)
+               $magicWords['NUMBEROFVOTES'] = array( 0, 'NUMBEROFVOTES' );
+               return true;
+       }
+
+       /**
+        * Assign a value to {{NUMBEROFVOTES}}. First we try memcached and if 
that
+        * fails, we fetch it directly from the database and cache it for 24 
hours.
+        *
+        * @param $parser Parser
+        * @param $cache
+        * @param $magicWordId String: magic word ID
+        * @param $ret Integer: return value (number of votes)
+        * @return Boolean: true
+        */
+       public static function assignValueToMagicWord( &$parser, &$cache, 
&$magicWordId, &$ret ) {
+               global $wgMemc;
+
+               if ( $magicWordId == 'NUMBEROFVOTES' ) {
+                       $key = wfMemcKey( 'vote', 'magic-word' );
+                       $data = $wgMemc->get( $key );
+                       if ( $data != '' ) {
+                               // We have it in cache? Oh goody, let's just 
use the cached value!
+                               wfDebugLog(
+                                       'VoteNY',
+                                       'Got the amount of votes from memcached'
+                               );
+                               // return value
+                               $ret = $data;
+                       } else {
+                               // Not cached → have to fetch it from the 
database
+                               $dbr = wfGetDB( DB_SLAVE );
+                               $voteCount = (int)$dbr->selectField(
+                                       'Vote',
+                                       'COUNT(*) AS count',
+                                       array(),
+                                       __METHOD__
+                               );
+                               wfDebugLog( 'VoteNY', 'Got the amount of votes 
from DB' );
+                               // Store the count in cache...
+                               // (86400 = seconds in a day)
+                               $wgMemc->set( $key, $voteCount, 86400 );
+                               // ...and return the value to the user
+                               $ret = $voteCount;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Register the magic word ID for {{NUMBEROFVOTES}}.
+        *
+        * @param $variableIds Array: array of pre-existing variable IDs
+        * @return Boolean: true
+        */
+       public static function registerVariableId( &$variableIds ) {
+               $variableIds[] = 'NUMBEROFVOTES';
+               return true;
+       }
+
+       /**
+        * Creates the necessary database table when the user runs
+        * maintenance/update.php.
+        *
+        * @param $updater Object: instance of DatabaseUpdater
+        * @return Boolean: true
+        */
+       public static function addTable( $updater = null ) {
+               $dir = dirname( __FILE__ );
+               $file = "$dir/vote.sql";
+               if ( $updater === null ) {
+                       global $wgExtNewTables;
+                       $wgExtNewTables[] = array( 'Vote', $file );
+               } else {
+                       $updater->addExtensionUpdate( array( 'addTable', 
'Vote', $file, true ) );
+               }
+               return true;
+       }
+}
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/VoteHooks.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/Vote_AjaxFunctions.php
===================================================================
--- trunk/extensions/VoteNY/Vote_AjaxFunctions.php                              
(rev 0)
+++ trunk/extensions/VoteNY/Vote_AjaxFunctions.php      2011-09-14 15:26:09 UTC 
(rev 97068)
@@ -0,0 +1,88 @@
+<?php
+/**
+ * AJAX functions used by Vote extension.
+ */
+$wgAjaxExportList[] = 'wfVoteClick';
+
+function wfVoteClick( $voteValue, $pageId, $mk ) {
+       global $wgUser;
+
+       if ( !$wgUser->isAllowed( 'vote' ) ) {
+               return '';
+       }
+
+       if( is_numeric( $pageId ) && ( is_numeric( $voteValue ) ) ) {
+               $vote = new Vote( $pageId );
+               $vote->insert( $voteValue );
+
+               return $vote->count( 1 );
+       } else {
+               return 'error';
+       }
+}
+
+$wgAjaxExportList[] = 'wfVoteDelete';
+function wfVoteDelete( $pageId, $mk ) {
+       global $wgUser;
+
+       if ( !$wgUser->isAllowed( 'vote' ) ) {
+               return '';
+       }
+
+       if( is_numeric( $pageId ) ) {
+               $vote = new Vote( $pageId );
+               $vote->delete();
+
+               return $vote->count( 1 );
+       } else {
+               return 'error';
+       }
+}
+
+$wgAjaxExportList[] = 'wfVoteStars';
+function wfVoteStars( $voteValue, $pageId, $mk ) {
+       global $wgUser;
+
+       if ( !$wgUser->isAllowed( 'vote' ) ) {
+               return '';
+       }
+
+       $vote = new VoteStars( $pageId );
+       if( $vote->UserAlreadyVoted() ) {
+               $vote->delete();
+       }
+       $vote->insert( $voteValue );
+
+       return $vote->display( $voteValue );
+}
+
+$wgAjaxExportList[] = 'wfVoteStarsMulti';
+function wfVoteStarsMulti( $voteValue, $pageId, $mk ) {
+       global $wgUser;
+
+       if ( !$wgUser->isAllowed( 'vote' ) ) {
+               return '';
+       }
+
+       $vote = new VoteStars( $pageId );
+       if( $vote->UserAlreadyVoted() ) {
+               $vote->delete();
+       }
+       $vote->insert( $voteValue );
+
+       return $vote->displayScore();
+}
+
+$wgAjaxExportList[] = 'wfVoteStarsDelete';
+function wfVoteStarsDelete( $pageId ) {
+       global $wgUser;
+
+       if ( !$wgUser->isAllowed( 'vote' ) ) {
+               return '';
+       }
+
+       $vote = new VoteStars( $pageId );
+       $vote->delete();
+
+       return $vote->display();
+}
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/Vote_AjaxFunctions.php
___________________________________________________________________
Added: svn:eol-style
   + native

Added: trunk/extensions/VoteNY/images/star_half.gif
===================================================================
(Binary files differ)


Property changes on: trunk/extensions/VoteNY/images/star_half.gif
___________________________________________________________________
Added: svn:mime-type
   + image/gif

Added: trunk/extensions/VoteNY/images/star_off.gif
===================================================================
(Binary files differ)


Property changes on: trunk/extensions/VoteNY/images/star_off.gif
___________________________________________________________________
Added: svn:mime-type
   + image/gif

Added: trunk/extensions/VoteNY/images/star_on.gif
===================================================================
(Binary files differ)


Property changes on: trunk/extensions/VoteNY/images/star_on.gif
___________________________________________________________________
Added: svn:mime-type
   + image/gif

Added: trunk/extensions/VoteNY/images/star_voted.gif
===================================================================
(Binary files differ)


Property changes on: trunk/extensions/VoteNY/images/star_voted.gif
___________________________________________________________________
Added: svn:mime-type
   + image/gif

Added: trunk/extensions/VoteNY/vote.sql
===================================================================
--- trunk/extensions/VoteNY/vote.sql                            (rev 0)
+++ trunk/extensions/VoteNY/vote.sql    2011-09-14 15:26:09 UTC (rev 97068)
@@ -0,0 +1,21 @@
+CREATE TABLE /*_*/Vote (
+  -- Internal ID to identify between different vote tags on different pages
+  `vote_id` int(11) NOT NULL auto_increment PRIMARY KEY,
+  -- Username (if any) of the person who voted
+  `username` varchar(255) NOT NULL default '0',
+  -- User ID of the person who voted
+  `vote_user_id` int(11) NOT NULL default '0',
+  -- ID of the page where the vote tag is in
+  `vote_page_id` int(11) NOT NULL default '0',
+  -- Value of the vote (ranging from 1 to 5)
+  `vote_value` char(1) character set latin1 collate latin1_bin NOT NULL 
default '',
+  -- Timestamp when the vote was cast
+  `vote_date` datetime NOT NULL default '0000-00-00 00:00:00',
+  -- IP address of the user who voted
+  `vote_ip` varchar(45) NOT NULL default ''
+) /*$wgDBTableOptions*/;
+
+CREATE INDEX vote_page_id_index ON /*_*/Vote (vote_page_id);
+CREATE INDEX valueidx ON /*_*/Vote (vote_value);
+CREATE INDEX usernameidx ON /*_*/Vote (username);
+CREATE INDEX vote_date ON /*_*/Vote (vote_date);
\ No newline at end of file


Property changes on: trunk/extensions/VoteNY/vote.sql
___________________________________________________________________
Added: svn:eol-style
   + native


_______________________________________________
MediaWiki-CVS mailing list
MediaWiki-CVS@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-cvs

Reply via email to