protected function showTable() {
		global $wgOut;
		# Show latest month of results
		$html = ReaderFeedback::getVoteAggregates( $this->page, $this->period, array(),
			($this->doPurge ? 'skipCache' : 'useCache')
		);
		if( $html != '' ) {
			$wgOut->addHTML(
				'<h2>' . wfMsgHtml('ratinghistory-table') . "</h2>\n" .
				wfMsgExt( 'ratinghistory-ratings', 'parse' ) . "\n" .
				"<div class='rfb-reader_feedback_ratings'>$html</div>"
			);
		}
	}
	public static function AjaxReview( /*$args...*/ ) {
		global $wgUser;
		$args = func_get_args();
		// Basic permission check
		if( $wgUser->isAllowed( 'feedback' ) ) {
			if( $wgUser->isBlocked() ) {
				return '<err#><h2>' . wfMsgHtml('blockedtitle') . '</h2>' .
					wfMsg('badaccess-group0');
			}
		} else {
			return '<err#><strong>' . wfMsg('badaccess-group0') . '</<strong>';
		}
		if( wfReadOnly() ) {
			return '<err#><strong>' . wfMsg('formerror') . '</<strong>';
		}
		$tags = ReaderFeedback::getFeedbackTags();
		// Make review interface object
		$form = new ReaderFeedbackPage();
		$form->dims = array();
		$unsureCount = 0;
		$bot = false;
		// Each ajax url argument is of the form param|val.
		// This means that there is no ugly order dependance.
		foreach( $args as $arg ) {
			$set = explode('|',$arg,2);
			if( count($set) != 2 ) {
				return '<err#>' . wfMsg('formerror');
			}
			list($par,$val) = $set;
			switch( $par )
			{
				case "target":
					$form->page = Title::newFromURL( $val );
					if( is_null($form->page) || !ReaderFeedback::isPageRateable( $form->page ) ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "oldid":
					$form->oldid = intval( $val );
					if( !$form->oldid ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "validatedParams":
					$form->validatedParams = $val;
					break;
				case "wpEditToken":
					if( !$wgUser->matchEditToken( $val ) ) {
						return '<err#>' . wfMsg('formerror');
					}
					break;
				case "commentary": // honeypot value
					if( $val )
						$bot = true;
					break;
				default:
					$p = preg_replace( '/^wp/', '', $par ); // kill any "wp" prefix
					if( array_key_exists( $p, $tags ) ) {
						$form->dims[$p] = intval($val);
						if( $form->dims[$p] === null ) { // nothing sent at all :(
							return '<err#>' . wfMsg('formerror'); // bad range
						} elseif( $form->dims[$p] === -1 ) {
							$unsureCount++;
						}
					}
					break;
			}
		}
		// Missing params?
		if( count($form->dims) != count($tags) || $unsureCount >= count($form->dims) ) {
			return '<err#>' . wfMsg('formerror');
		}
		// Doesn't match up?
		if( $form->validatedParams != self::validationKey( $form->oldid, $wgUser->getId() ) ) {
			return '<err#>' . wfMsg('formerror');
		}
		$rhist = SpecialPage::getTitleFor( 'RatingHistory' );
		$graphLink = $rhist->getFullUrl( 'target='.$form->page->getPrefixedUrl() );
		$talk = $form->page->getTalkPage();

		$tallyTable = ReaderFeedback::getVoteAggregates( $form->page, 31, $form->dims );
		
		$dbw = wfGetDB( DB_MASTER );
		$dbw->begin();
		if( $bot ) {
			$ok = self::REVIEW_ERROR; // don't submit for mindless drones
		} else {
			$ok = $form->submit();
		}
		$dbw->commit();
		switch( $ok ) {
			case self::REVIEW_OK:
				return '<suc#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-success', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
						'<h4>'.wfMsgHtml('ratinghistory-table')."</h4>\n$tallyTable" .
					"</div>";
			case self::REVIEW_DUP:
				return '<err#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-voted', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
					"</div>";
			default:
				return '<err#>' .
					"<div class='plainlinks'>" .
						wfMsgExt( 'readerfeedback-error', array('parseinline'), 
							$form->page->getPrefixedText(), $graphLink,
							$talk->getFullUrl( 'action=edit&section=new' ) ) .
					"</div>";
		}
	}