public function execute( $par ) {
		global $wgRequest, $wgUser, $wgOut;
		$this->setHeaders();
		if( $wgUser->isAllowed( 'feedback' ) ) {
			if( $wgUser->isBlocked() ) {
				$wgOut->blockedPage();
				return;
			}
		} else {
			$wgOut->permissionRequired( 'feedback' );
			return;
		}
		if( wfReadOnly() ) {
			$wgOut->readOnlyPage();
			return;
		}
		$this->skin = $wgUser->getSkin();
		$this->doPurge = ('purge' === $wgRequest->getVal( 'action' ) && $wgUser->isAllowed('purge'));
		# Our target page
		$this->target = $wgRequest->getText( 'target' );
		$this->page = Title::newFromURL( $this->target );
		# We need a page...
		if( is_null($this->page) ) {
			$wgOut->showErrorPage( 'notargettitle', 'notargettext' );
			return;
		} elseif( !ReaderFeedback::isPageRateable( $this->page ) ) {
			$wgOut->addHTML( wfMsgExt('readerfeedback-main',array('parse')) );
			return;
		}
		# Thank people who voted...
		if( ReaderFeedbackPage::userAlreadyVoted( $this->page ) ) {
			$wgOut->setSubtitle( wfMsgExt('ratinghistory-thanks','parseinline') );
		}
		$period = $wgRequest->getInt( 'period' );
		$validPeriods = array(31,93,365,1095);
		if( !in_array($period,$validPeriods) ) {
			$period = 31; // default
		}
		$this->period = $period;
		$this->dScale = 20;

		$this->now = time(); // one time for whole request

		$this->showForm();
		$this->showTable();
		/*
		 * Allow client caching.
		 */
		if( !$this->doPurge && $wgOut->checkLastModified( self::getTouched($this->page) ) ) {
			return; // Client cache fresh and headers sent, nothing more to do
		} else {
			$wgOut->enableClientCache( false ); // don't show stale graphs
			if( $this->doPurge ) $this->purgePage();
		}
		$this->showGraphs();
	}
	public static function addRatingLink( &$skintemplate, &$nav_urls, &$oldid, &$revid ) {
		# Add rating tab
		if( $skintemplate->getTitle() && ReaderFeedback::isPageRateable( $skintemplate->getTitle() ) ) {
			$nav_urls['ratinghist'] = array( 
				'text' => wfMsg( 'ratinghistory-link' ),
				'href' => $skintemplate->makeSpecialUrl( 'RatingHistory', 
					"target=" . wfUrlencode( "{$skintemplate->thispage}" ) )
			);
		}
		return true;
	}
	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>";
		}
	}