r13224 - Code Review

From MediaWiki.org

Jump to: navigation, search
Repository:MediaWiki
Revision:r13223 | r13224 (on ViewVC) | r13225 >
Date:19:04, 16 March 2006
Author:vibber
Status:new
Tags:
Comment:* Further work on rev_deleted; changed to a bitfield with several data-hiding
options. Not yet ready for production use; Special:Revisiondelete is
incomplete, and the flags are not preserved across page deletion/undeletion.
To try it; add the 'deleterevision' permission to a privileged group.
Also split some functions from GlobalFunctions to XmlFunctions.php, added
some convenience functions for building form controls, some more Linker
conveniences for formatting various types of standard link clusters.
Modified paths:

Diff [purge]

Index: trunk/phase3/skins/monobook/main.css
===================================================================
--- trunk/phase3/skins/monobook/main.css	(revision 13223)
+++ trunk/phase3/skins/monobook/main.css	(revision 13224)
@@ -341,7 +341,8 @@
 */
 
 #toc,
-.toc {
+.toc,
+.mw-warning {
 	border: 1px solid #aaa;
 	background-color: #f9f9f9;
 	padding: 5px;
@@ -378,6 +379,11 @@
 	font-size: 94%;
 }
 
+.mw-warning {
+	margin-left: 50px;
+	margin-right: 50px;
+	text-align: center;
+}
 
 /* images */
 div.floatright, table.floatright {
Index: trunk/phase3/includes/XmlFunctions.php
===================================================================
--- trunk/phase3/includes/XmlFunctions.php	(revision 0)
+++ trunk/phase3/includes/XmlFunctions.php	(revision 13224)
@@ -0,0 +1,273 @@
+<?php
+
+/**
+ * Format an XML element with given attributes and, optionally, text content.
+ * Element and attribute names are assumed to be ready for literal inclusion.
+ * Strings are assumed to not contain XML-illegal characters; special
+ * characters (<, >, &) are escaped but illegals are not touched.
+ *
+ * @param string $element
+ * @param array $attribs Name=>value pairs. Values will be escaped.
+ * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @return string
+ */
+function wfElement( $element, $attribs = null, $contents = '') {
+	$out = '<' . $element;
+	if( !is_null( $attribs ) ) {
+		foreach( $attribs as $name => $val ) {
+			$out .= ' ' . $name . '="' . htmlspecialchars( $val ) . '"';
+		}
+	}
+	if( is_null( $contents ) ) {
+		$out .= '>';
+	} else {
+		if( $contents == '' ) {
+			$out .= ' />';
+		} else {
+			$out .= '>' . htmlspecialchars( $contents ) . "</$element>";
+		}
+	}
+	return $out;
+}
+
+/**
+ * Format an XML element as with wfElement(), but run text through the
+ * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
+ * is passed.
+ *
+ * @param string $element
+ * @param array $attribs Name=>value pairs. Values will be escaped.
+ * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
+ * @return string
+ */
+function wfElementClean( $element, $attribs = array(), $contents = '') {
+	if( $attribs ) {
+		$attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
+	}
+	if( $contents ) {
+		$contents = UtfNormal::cleanUp( $contents );
+	}
+	return wfElement( $element, $attribs, $contents );
+}
+
+// Shortcuts
+function wfOpenElement( $element, $attribs = null ) { return wfElement( $element, $attribs, null ); }
+function wfCloseElement( $element ) { return "</$element>"; }
+
+/**
+ * Create a namespace selector
+ *
+ * @param mixed $selected The namespace which should be selected, default ''
+ * @param string $allnamespaces Value of a special item denoting all namespaces. Null to not include (default)
+ * @param bool $includehidden Include hidden namespaces?
+ * @return Html string containing the namespace selector
+ */
+function &HTMLnamespaceselector($selected = '', $allnamespaces = null, $includehidden=false) {
+	global $wgContLang;
+	if( $selected !== '' ) {
+		if( is_null( $selected ) ) {
+			// No namespace selected; let exact match work without hitting Main
+			$selected = '';
+		} else {
+			// Let input be numeric strings without breaking the empty match.
+			$selected = intval( $selected );
+		}
+	}
+	$s = "<select name='namespace' class='namespaceselector'>\n\t";
+	$arr = $wgContLang->getFormattedNamespaces();
+	if( !is_null($allnamespaces) ) {
+		$arr = array($allnamespaces => wfMsgHtml('namespacesall')) + $arr;
+	}
+	foreach ($arr as $index => $name) {
+		if ($index < NS_MAIN) continue;
+
+		$name = $index !== 0 ? $name : wfMsgHtml('blanknamespace');
+
+		if ($index === $selected) {
+			$s .= wfElement("option",
+					array("value" => $index, "selected" => "selected"),
+					$name);
+		} else {
+			$s .= wfElement("option", array("value" => $index), $name);
+		}
+	}
+	$s .= "\n</select>\n";
+	return $s;
+}
+
+function wfSpan( $text, $class, $attribs=array() ) {
+	return wfElement( 'span', array( 'class' => $class ) + $attribs, $text );
+}
+
+/**
+ * Convenience function to build an HTML text input field
+ * @return string HTML
+ */
+function wfInput( $name, $size=false, $value=false, $attribs=array() ) {
+	return wfElement( 'input', array(
+		'name' => $name,
+		'size' => $size,
+		'value' => $value ) + $attribs );
+}
+
+/**
+ * Internal function for use in checkboxes and radio buttons and such.
+ * @return array
+ */
+function wfAttrib( $name, $present = true ) {
+	return $present ? array( $name => $name ) : array();
+}
+
+/**
+ * Convenience function to build an HTML checkbox
+ * @return string HTML
+ */
+function wfCheck( $name, $checked=false, $attribs=array() ) {
+	return wfElement( 'input', array(
+		'name' => $name,
+		'type' => 'checkbox',
+		'value' => 1 ) + wfAttrib( 'checked', $checked ) +  $attribs );
+}
+
+/**
+ * Convenience function to build an HTML radio button
+ * @return string HTML
+ */
+function wfRadio( $name, $value, $checked=false, $attribs=array() ) {
+	return wfElement( 'input', array(
+		'name' => $name,
+		'type' => 'radio',
+		'value' => $value ) + wfAttrib( 'checked', $checked ) + $attribs );
+}
+
+/**
+ * Convenience function to build an HTML form label
+ * @return string HTML
+ */
+function wfLabel( $label, $id ) {
+	return wfElement( 'label', array( 'for' => $id ), $label );
+}
+
+/**
+ * Convenience function to build an HTML text input field with a label
+ * @return string HTML
+ */
+function wfInputLabel( $label, $name, $id, $size=false, $value=false, $attribs=array() ) {
+	return wfLabel( $label, $id ) .
+		'&nbsp;' .
+		wfInput( $name, $size, $value, array( 'id' => $id ) + $attribs );
+}
+
+/**
+ * Convenience function to build an HTML checkbox with a label
+ * @return string HTML
+ */
+function wfCheckLabel( $label, $name, $id, $checked=false, $attribs=array() ) {
+	return wfCheck( $name, $checked, array( 'id' => $id ) + $attribs ) .
+		'&nbsp;' .
+		wfLabel( $label, $id );
+}
+
+/**
+ * Convenience function to build an HTML radio button with a label
+ * @return string HTML
+ */
+function wfRadioLabel( $label, $name, $value, $id, $checked=false, $attribs=array() ) {
+	return wfRadio( $name, $checked, $value, array( 'id' => $id ) + $attribs ) .
+		'&nbsp;' .
+		wfLabel( $label, $id );
+}
+
+/**
+ * Convenience function to build an HTML submit button
+ * @param string $value Label text for the button
+ * @param array $attribs optional custom attributes
+ * @return string HTML
+ */
+function wfSubmitButton( $value, $attribs=array() ) {
+	return wfElement( 'input', array( 'type' => 'submit', 'value' => $value ) + $attribs );
+}
+
+/**
+ * Convenience function to build an HTML hidden form field
+ * @param string $value Label text for the button
+ * @param array $attribs optional custom attributes
+ * @return string HTML
+ */
+function wfHidden( $name, $value, $attribs=array() ) {
+	return wfElement( 'input', array(
+		'name' => $name,
+		'type' => 'hidden',
+		'value' => $value ) + $attribs );
+}
+
+/**
+ * Returns an escaped string suitable for inclusion in a string literal
+ * for JavaScript source code.
+ * Illegal control characters are assumed not to be present.
+ *
+ * @param string $string
+ * @return string
+ */
+function wfEscapeJsString( $string ) {
+	// See ECMA 262 section 7.8.4 for string literal format
+	$pairs = array(
+		"\\" => "\\\\",
+		"\"" => "\\\"",
+		'\'' => '\\\'',
+		"\n" => "\\n",
+		"\r" => "\\r",
+
+		# To avoid closing the element or CDATA section
+		"<" => "\\x3c",
+		">" => "\\x3e",
+	);
+	return strtr( $string, $pairs );
+}
+
+/**
+ * Check if a string is well-formed XML.
+ * Must include the surrounding tag.
+ *
+ * @param string $text
+ * @return bool
+ *
+ * @todo Error position reporting return
+ */
+function wfIsWellFormedXml( $text ) {
+	$parser = xml_parser_create( "UTF-8" );
+
+	# case folding violates XML standard, turn it off
+	xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
+
+	if( !xml_parse( $parser, $text, true ) ) {
+		$err = xml_error_string( xml_get_error_code( $parser ) );
+		$position = xml_get_current_byte_index( $parser );
+		//$fragment = $this->extractFragment( $html, $position );
+		//$this->mXmlError = "$err at byte $position:\n$fragment";
+		xml_parser_free( $parser );
+		return false;
+	}
+	xml_parser_free( $parser );
+	return true;
+}
+
+/**
+ * Check if a string is a well-formed XML fragment.
+ * Wraps fragment in an <html> bit and doctype, so it can be a fragment
+ * and can use HTML named entities.
+ *
+ * @param string $text
+ * @return bool
+ */
+function wfIsWellFormedXmlFragment( $text ) {
+	$html =
+		Sanitizer::hackDocType() .
+		'<html>' .
+		$text .
+		'</html>';
+	return wfIsWellFormedXml( $html );
+}
+
+
+?>
\ No newline at end of file

Property changes on: trunk/phase3/includes/XmlFunctions.php
___________________________________________________________________
Name: svn:eol-style
   + native
Name: svn:keywords
   + Author Date Id Revision

Index: trunk/phase3/includes/Article.php
===================================================================
--- trunk/phase3/includes/Article.php	(revision 13223)
+++ trunk/phase3/includes/Article.php	(revision 13224)
@@ -495,7 +495,10 @@
 			}
 		}
 
-		$this->mContent   = $revision->getText();
+		// FIXME: Horrible, horrible! This content-loading interface just plain sucks.
+		// We should instead work with the Revision object when we need it...
+		$this->mContent = $revision->userCan( MW_REV_DELETED_TEXT ) ? $revision->getRawText() : "";
+		//$this->mContent   = $revision->getText();
 
 		$this->mUser      = $revision->getUser();
 		$this->mUserText  = $revision->getUserText();
@@ -767,7 +770,7 @@
 			wfProfileOut( $fname );
 			return;
 		}
-
+		
 		if ( empty( $oldid ) && $this->checkTouched() ) {
 			$wgOut->setETag($parserCache->getETag($this, $wgUser));
 
@@ -846,7 +849,18 @@
 
 			if ( !empty( $oldid ) ) {
 				$this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid );
-				$wgOut->setRobotpolicy( 'noindex,follow' );
+				$wgOut->setRobotpolicy( 'noindex,nofollow' );
+				if( $this->mRevision->isDeleted( MW_REV_DELETED_TEXT ) ) {
+					if( !$this->mRevision->userCan( MW_REV_DELETED_TEXT ) ) {
+						$wgOut->addWikiText( wfMsg( 'rev-deleted-text-permission' ) );
+						$wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
+						return;
+					} else {
+						$wgOut->addWikiText( wfMsg( 'rev-deleted-text-view' ) );
+						// and we are allowed to see...
+					}
+				}
+
 			}
 		}
 		if( !$outputDone ) {
Index: trunk/phase3/includes/GlobalFunctions.php
===================================================================
--- trunk/phase3/includes/GlobalFunctions.php	(revision 13223)
+++ trunk/phase3/includes/GlobalFunctions.php	(revision 13224)
@@ -30,6 +30,7 @@
 require_once( 'UpdateClasses.php' );
 require_once( 'LogPage.php' );
 require_once( 'normal/UtfNormalUtil.php' );
+require_once( 'XmlFunctions.php' );
 
 /**
  * Compatibility functions
@@ -839,30 +840,7 @@
 	return $out;
 }
 
-/**
- * Returns an escaped string suitable for inclusion in a string literal
- * for JavaScript source code.
- * Illegal control characters are assumed not to be present.
- *
- * @param string $string
- * @return string
- */
-function wfEscapeJsString( $string ) {
-	// See ECMA 262 section 7.8.4 for string literal format
-	$pairs = array(
-		"\\" => "\\\\",
-		"\"" => "\\\"",
-		'\'' => '\\\'',
-		"\n" => "\\n",
-		"\r" => "\\r",
 
-		# To avoid closing the element or CDATA section
-		"<" => "\\x3c",
-		">" => "\\x3e",
-	);
-	return strtr( $string, $pairs );
-}
-
 /**
  * @todo document
  * @return float
@@ -873,15 +851,6 @@
 }
 
 /**
- * Changes the first character to an HTML entity
- */
-function wfHtmlEscapeFirst( $text ) {
-	$ord = ord($text);
-	$newText = substr($text, 1);
-	return "&#$ord;$newText";
-}
-
-/**
  * Sets dest to source and returns the original value of dest
  * If source is NULL, it just returns the value, it doesn't set the variable
  */
@@ -1453,100 +1422,6 @@
 	return( $siteNotice );
 }
 
-/**
- * Format an XML element with given attributes and, optionally, text content.
- * Element and attribute names are assumed to be ready for literal inclusion.
- * Strings are assumed to not contain XML-illegal characters; special
- * characters (<, >, &) are escaped but illegals are not touched.
- *
- * @param string $element
- * @param array $attribs Name=>value pairs. Values will be escaped.
- * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
- * @return string
- */
-function wfElement( $element, $attribs = null, $contents = '') {
-	$out = '<' . $element;
-	if( !is_null( $attribs ) ) {
-		foreach( $attribs as $name => $val ) {
-			$out .= ' ' . $name . '="' . htmlspecialchars( $val ) . '"';
-		}
-	}
-	if( is_null( $contents ) ) {
-		$out .= '>';
-	} else {
-		if( $contents == '' ) {
-			$out .= ' />';
-		} else {
-			$out .= '>' . htmlspecialchars( $contents ) . "</$element>";
-		}
-	}
-	return $out;
-}
-
-/**
- * Format an XML element as with wfElement(), but run text through the
- * UtfNormal::cleanUp() validator first to ensure that no invalid UTF-8
- * is passed.
- *
- * @param string $element
- * @param array $attribs Name=>value pairs. Values will be escaped.
- * @param string $contents NULL to make an open tag only; '' for a contentless closed tag (default)
- * @return string
- */
-function wfElementClean( $element, $attribs = array(), $contents = '') {
-	if( $attribs ) {
-		$attribs = array_map( array( 'UtfNormal', 'cleanUp' ), $attribs );
-	}
-	if( $contents ) {
-		$contents = UtfNormal::cleanUp( $contents );
-	}
-	return wfElement( $element, $attribs, $contents );
-}
-
-// Shortcuts
-function wfOpenElement( $element, $attribs = null ) { return wfElement( $element, $attribs, null ); }
-function wfCloseElement( $element ) { return "</$element>"; }
-
-/**
- * Create a namespace selector
- *
- * @param mixed $selected The namespace which should be selected, default ''
- * @param string $allnamespaces Value of a special item denoting all namespaces. Null to not include (default)
- * @return Html string containing the namespace selector
- */
-function &HTMLnamespaceselector($selected = '', $allnamespaces = null) {
-	global $wgContLang;
-	if( $selected !== '' ) {
-		if( is_null( $selected ) ) {
-			// No namespace selected; let exact match work without hitting Main
-			$selected = '';
-		} else {
-			// Let input be numeric strings without breaking the empty match.
-			$selected = intval( $selected );
-		}
-	}
-	$s = "<select id='namespace' name='namespace' class='namespaceselector'>\n\t";
-	$arr = $wgContLang->getFormattedNamespaces();
-	if( !is_null($allnamespaces) ) {
-		$arr = array($allnamespaces => wfMsgHtml('namespacesall')) + $arr;
-	}
-	foreach ($arr as $index => $name) {
-		if ($index < NS_MAIN) continue;
-
-		$name = $index !== 0 ? $name : wfMsgHtml('blanknamespace');
-
-		if ($index === $selected) {
-			$s .= wfElement("option",
-					array("value" => $index, "selected" => "selected"),
-					$name);
-		} else {
-			$s .= wfElement("option", array("value" => $index), $name);
-		}
-	}
-	$s .= "\n</select>\n";
-	return $s;
-}
-
 /** Global singleton instance of MimeMagic. This is initialized on demand,
 * please always use the wfGetMimeMagic() function to get the instance.
 *
@@ -1727,50 +1602,6 @@
 }
 
 /**
- * Check if a string is well-formed XML.
- * Must include the surrounding tag.
- *
- * @param string $text
- * @return bool
- *
- * @todo Error position reporting return
- */
-function wfIsWellFormedXml( $text ) {
-	$parser = xml_parser_create( "UTF-8" );
-
-	# case folding violates XML standard, turn it off
-	xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false );
-
-	if( !xml_parse( $parser, $text, true ) ) {
-		$err = xml_error_string( xml_get_error_code( $parser ) );
-		$position = xml_get_current_byte_index( $parser );
-		//$fragment = $this->extractFragment( $html, $position );
-		//$this->mXmlError = "$err at byte $position:\n$fragment";
-		xml_parser_free( $parser );
-		return false;
-	}
-	xml_parser_free( $parser );
-	return true;
-}
-
-/**
- * Check if a string is a well-formed XML fragment.
- * Wraps fragment in an <html> bit and doctype, so it can be a fragment
- * and can use HTML named entities.
- *
- * @param string $text
- * @return bool
- */
-function wfIsWellFormedXmlFragment( $text ) {
-	$html =
-		Sanitizer::hackDocType() .
-		'<html>' .
-		$text .
-		'</html>';
-	return wfIsWellFormedXml( $html );
-}
-
-/**
  * shell_exec() with time and memory limits mirrored from the PHP configuration,
  * if supported.
  */
Index: trunk/phase3/includes/SpecialRevisiondelete.php
===================================================================
--- trunk/phase3/includes/SpecialRevisiondelete.php	(revision 0)
+++ trunk/phase3/includes/SpecialRevisiondelete.php	(revision 13224)
@@ -0,0 +1,258 @@
+<?php
+
+/**
+ * Not quite ready for production use yet; need to fix up the restricted mode,
+ * and provide for preservation across delete/undelete of the page.
+ *
+ * To try this out, set up extra permissions something like:
+ * $wgGroupPermissions['sysop']['deleterevision'] = true;
+ * $wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+ */
+
+function wfSpecialRevisiondelete( $par = null ) {
+	global $wgOut, $wgRequest, $wgUser, $wgContLang;
+	
+	$target = $wgRequest->getVal( 'target' );
+	$oldid = $wgRequest->getInt( 'oldid' );
+	
+	$sk = $wgUser->getSkin();
+	$page = Title::newFromUrl( $target );
+	
+	if( is_null( $page ) ) {
+		$wgOut->errorpage( 'notargettitle', 'notargettext' );
+		return;
+	}
+	
+	$form = new RevisionDeleteForm( $wgRequest );
+	if( $wgRequest->wasPosted() ) {
+		$form->submit( $wgRequest );
+	} else {
+		$form->show( $wgRequest );
+	}
+}
+
+class RevisionDeleteForm {
+	/**
+	 * @param Title $page
+	 * @param int $oldid
+	 */
+	function __construct( $request ) {
+		global $wgUser;
+		
+		$target = $request->getVal( 'target' );
+		$this->page = Title::newFromUrl( $target );
+		
+		$this->revisions = $request->getIntArray( 'oldid', array() );
+		
+		$this->skin = $wgUser->getSkin();
+		$this->checks = array(
+			array( 'revdelete-hide-text', 'wpHideText', MW_REV_DELETED_TEXT ),
+			array( 'revdelete-hide-comment', 'wpHideComment', MW_REV_DELETED_COMMENT ),
+			array( 'revdelete-hide-user', 'wpHideUser', MW_REV_DELETED_USER ),
+			array( 'revdelete-hide-restricted', 'wpHideRestricted', MW_REV_DELETED_RESTRICTED ) );
+	}
+	
+	/**
+	 * @param WebRequest $request
+	 */
+	function show( $request ) {
+		global $wgOut, $wgUser;
+
+		$first = $this->revisions[0];
+		
+		$wgOut->addWikiText( wfMsg( 'revdelete-selected', $this->page->getPrefixedText() ) );
+		
+		$wgOut->addHtml( "<ul>" );
+		foreach( $this->revisions as $revid ) {
+			$rev = Revision::newFromTitle( $this->page, $revid );
+			$wgOut->addHtml( $this->historyLine( $rev ) );
+			$bitfields[] = $rev->mDeleted; // FIXME
+		}
+		$wgOut->addHtml( "</ul>" );
+	
+		$wgOut->addWikiText( wfMsg( 'revdelete-text' ) );
+		
+		$items = array(
+			wfInputLabel( wfMsg( 'revdelete-log' ), 'wpReason', 'wpReason', 60 ),
+			wfSubmitButton( wfMsg( 'revdelete-submit' ) ) );
+		$hidden = array(
+			wfHidden( 'wpEditToken', $wgUser->editToken() ),
+			wfHidden( 'target', $this->page->getPrefixedText() ) );
+		foreach( $this->revisions as $revid ) {
+			$hidden[] = wfHidden( 'oldid[]', $revid );
+		}
+		
+		$special = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' );
+		$wgOut->addHtml( wfElement( 'form', array(
+			'method' => 'post',
+			'action' => $special->getLocalUrl( 'action=submit' ) ) ) );
+		
+		$wgOut->addHtml( '<fieldset><legend>' . wfMsgHtml( 'revdelete-legend' ) . '</legend>' );
+		foreach( $this->checks as $item ) {
+			list( $message, $name, $field ) = $item;
+			$wgOut->addHtml( '<div>' .
+				wfCheckLabel( wfMsg( $message), $name, $name, $rev->isDeleted( $field ) ) .
+				'</div>' );
+		}
+		$wgOut->addHtml( '</fieldset>' );
+		foreach( $items as $item ) {
+			$wgOut->addHtml( '<p>' . $item . '</p>' );
+		}
+		foreach( $hidden as $item ) {
+			$wgOut->addHtml( $item );
+		}
+		
+		$wgOut->addHtml( '</form>' );
+	}
+	
+	/**
+	 * @param Revision $rev
+	 * @returns string
+	 */
+	private function historyLine( $rev ) {
+		global $wgContLang;
+		$date = $wgContLang->timeanddate( $rev->getTimestamp() );
+		return
+			"<li>" .
+			$this->skin->makeLinkObj( $this->page, $date, 'oldid=' . $rev->getId() ) .
+			" " .
+			$this->skin->revUserLink( $rev ) .
+			" " .
+			$this->skin->revComment( $rev ) .
+			"</li>";
+	}
+	
+	/**
+	 * @param WebRequest $request
+	 */
+	function submit( $request ) {
+		$bitfield = $this->extractBitfield( $request );
+		$comment = $request->getText( 'wpReason' );
+		if( $this->save( $bitfield, $comment ) ) {
+			return $this->success( $request );
+		} else {
+			return $this->show( $request );
+		}
+	}
+	
+	function success( $request ) {
+		global $wgOut;
+		$wgOut->addWikiText( 'woo' );
+	}
+	
+	/**
+	 * Put together a rev_deleted bitfield from the submitted checkboxes
+	 * @param WebRequest $request
+	 * @return int
+	 */
+	function extractBitfield( $request ) {
+		$bitfield = 0;
+		foreach( $this->checks as $item ) {
+			list( $message, $name, $field ) = $item;
+			if( $request->getCheck( $name ) ) {
+				$bitfield |= $field;
+			}
+		}
+		return $bitfield;
+	}
+	
+	function save( $bitfield, $reason ) {
+		$dbw = wfGetDB( DB_MASTER );
+		$deleter = new RevisionDeleter( $dbw );
+		$ok = $deleter->setVisibility( $this->revisions, $bitfield, $reason );
+	}
+}
+
+
+class RevisionDeleter {
+	function __construct( $db ) {
+		$this->db = $db;
+	}
+	
+	/**
+	 * @param array $items list of revision ID numbers
+	 * @param int $bitfield new rev_deleted value
+	 * @param string $comment Comment for log records
+	 */
+	function setVisibility( $items, $bitfield, $comment ) {
+		$pages = array();
+		
+		// To work!
+		foreach( $items as $revid ) {
+			$rev = Revision::newFromId( $revid );
+			$this->updateRevision( $rev, $bitfield );
+			$this->updateRecentChanges( $rev, $bitfield );
+			
+			// For logging, maintain a count of revisions per page
+			$pageid = $rev->getPage();
+			if( isset( $pages[$pageid] ) ) {
+				$pages[$pageid]++;
+			} else {
+				$pages[$pageid] = 1;
+			}
+		}
+		
+		// Clear caches...
+		foreach( $pages as $pageid => $count ) {
+			$title = Title::newFromId( $pageid );
+			$this->updatePage( $title );
+			$this->updateLog( $title, $count, $bitfield, $comment );
+		}
+		
+		return true;
+	}
+	
+	/**
+	 * Update the revision's rev_deleted field
+	 * @param Revision $rev
+	 * @param int $bitfield new rev_deleted bitfield value
+	 */
+	function updateRevision( $rev, $bitfield ) {
+		$this->db->update( 'revision',
+			array( 'rev_deleted' => $bitfield ),
+			array( 'rev_id' => $rev->getId() ),
+			'RevisionDeleter::updateRevision' );
+	}
+	
+	/**
+	 * Update the revision's recentchanges record if fields have been hidden
+	 * @param Revision $rev
+	 * @param int $bitfield new rev_deleted bitfield value
+	 */
+	function updateRecentChanges( $rev, $bitfield ) {
+		$this->db->update( 'recentchanges',
+			array(
+				'rc_user' => ($bitfield & MW_REV_DELETED_USER) ? 0 : $rev->getUser(),
+				'rc_user_text' => ($bitfield & MW_REV_DELETED_USER) ? wfMsg( 'rev-deleted-user' ) : $rev->getUserText(),
+				'rc_comment' => ($bitfield & MW_REV_DELETED_COMMENT) ? wfMsg( 'rev-deleted-comment' ) : $rev->getComment() ),
+			array(
+				'rc_this_oldid' => $rev->getId() ),
+			'RevisionDeleter::updateRecentChanges' );
+	}
+	
+	/**
+	 * Touch the page's cache invalidation timestamp; this forces cached
+	 * history views to refresh, so any newly hidden or shown fields will
+	 * update properly.
+	 * @param Title $title
+	 */
+	function updatePage( $title ) {
+		$title->invalidateCache();
+	}
+	
+	/**
+	 * Record a log entry on the action
+	 * @param Title $title
+	 * @param int $count the number of revisions altered for this page
+	 * @param int $bitfield the new rev_deleted value
+	 * @param string $comment
+	 */
+	function updateLog( $title, $count, $bitfield, $comment ) {
+		$log = new LogPage( 'delete' );
+		$reason = "changed $count revisions to $bitfield";
+		$reason .= ": $comment";
+		$log->addEntry( 'revision', $title, $reason );
+	}
+}
+
+?>

Property changes on: trunk/phase3/includes/SpecialRevisiondelete.php
___________________________________________________________________
Name: svn:eol-style
   + native
Name: svn:keywords
   + Author Date Id Revision

Index: trunk/phase3/includes/Linker.php
===================================================================
--- trunk/phase3/includes/Linker.php	(revision 13223)
+++ trunk/phase3/includes/Linker.php	(revision 13224)
@@ -737,6 +737,115 @@
 	}
 
 	/**
+	 * Make user link (or user contributions for unregistered users)
+	 * @param int $userId
+	 * @param string $userText
+	 * @return string HTML fragment
+	 * @access private
+	 */
+	function userLink( $userId, $userText ) {
+		$encName = htmlspecialchars( $userText );
+		if( $userId == 0 ) {
+			$contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' );
+			return $this->makeKnownLinkObj( $contribsPage,
+				$encName, 'target=' . urlencode( $userText ) );
+		} else {
+			$userPage = Title::makeTitle( NS_USER, $userText );
+			return $this->makeLinkObj( $userPage, $encName );
+		}
+	}
+
+	/**
+	 * @param int $userId
+	 * @param string $userText
+	 * @return string HTML fragment with talk and/or block links
+	 * @access private
+	 */
+	function userToolLinks( $userId, $userText ) {
+		global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans;
+		$talkable = !( $wgDisableAnonTalk && 0 == $userId );
+		$blockable = ( $wgSysopUserBans || 0 == $userId );
+
+		$items = array();
+		if( $talkable ) {
+			$items[] = $this->userTalkLink( $userId, $userText );
+		}
+		if( $blockable && $wgUser->isAllowed( 'block' ) ) {
+			$items[] = $this->blockLink( $userId, $userText );
+		}
+
+		if( $items ) {
+			return ' (' . implode( ' | ', $items ) . ')';
+		} else {
+			return '';
+		}
+	}
+
+	/**
+	 * @param int $userId
+	 * @param string $userText
+	 * @return string HTML fragment with user talk link
+	 * @access private
+	 */
+	function userTalkLink( $userId, $userText ) {
+		global $wgContLang;
+		$talkname = $wgContLang->getNsText( NS_TALK ); # use the shorter name
+
+		$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
+		$userTalkLink = $this->makeLinkObj( $userTalkPage, $talkname );
+		return $userTalkLink;
+	}
+
+	/**
+	 * @param int $userId
+	 * @param string $userText
+	 * @return string HTML fragment with block link
+	 * @access private
+	 */
+	function blockLink( $userId, $userText ) {
+		$blockPage = Title::makeTitle( NS_SPECIAL, 'Blockip' );
+		$blockLink = $this->makeKnownLinkObj( $blockPage,
+			wfMsgHtml( 'blocklink' ), 'ip=' . urlencode( $userText ) );
+		return $blockLink;
+	}
+	
+	/**
+	 * Generate a user link if the current user is allowed to view it
+	 * @param Revision $rev
+	 * @return string HTML
+	 */
+	function revUserLink( $rev ) {
+		if( $rev->userCan( MW_REV_DELETED_USER ) ) {
+			$link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() );
+		} else {
+			$link = wfMsgHtml( 'rev-deleted-user' );
+		}
+		if( $rev->isDeleted( MW_REV_DELETED_USER ) ) {
+			return '<span class="history-deleted">' . $link . '</span>';
+		}
+		return $link;
+	}
+
+	/**
+	 * Generate a user tool link cluster if the current user is allowed to view it
+	 * @param Revision $rev
+	 * @return string HTML
+	 */
+	function revUserTools( $rev ) {
+		if( $rev->userCan( MW_REV_DELETED_USER ) ) {
+			$link = $this->userLink( $rev->getRawUser(), $rev->getRawUserText() ) .
+				' ' .
+				$this->userToolLinks( $rev->getRawUser(), $rev->getRawUserText() );
+		} else {
+			$link = wfMsgHtml( 'rev-deleted-user' );
+		}
+		if( $rev->isDeleted( MW_REV_DELETED_USER ) ) {
+			return '<span class="history-deleted">' . $link . '</span>';
+		}
+		return $link;
+	}
+	
+	/**
 	 * This function is called by all recent changes variants, by the page history,
 	 * and by the user contributions list. It is responsible for formatting edit
 	 * comments. It escapes any HTML in the comment, but adds some CSS to format
@@ -823,25 +932,39 @@
 	 *
 	 * @param string $comment
 	 * @param Title $title
-	 * @param bool $deleted
 	 *
 	 * @return string
 	 */
-	function commentBlock( $comment, $title = NULL, $deleted = false ) {
+	function commentBlock( $comment, $title = NULL ) {
 		// '*' used to be the comment inserted by the software way back
 		// in antiquity in case none was provided, here for backwards
 		// compatability, acc. to brion -ævar
 		if( $comment == '' || $comment == '*' ) {
 			return '';
 		} else {
-			if ( $deleted )
-				return " <span class='comment'>(...)</span>";
-			else {
-				$formatted = $this->formatComment( $comment, $title );
-				return " <span class='comment'>($formatted)</span>";
-			}
+			$formatted = $this->formatComment( $comment, $title );
+			return " <span class='comment'>($formatted)</span>";
 		}
 	}
+	
+	/**
+	 * Wrap and format the given revision's comment block, if the current
+	 * user is allowed to view it.
+	 * @param Revision $rev
+	 * @return string HTML
+	 */
+	function revComment( $rev ) {
+		if( $rev->userCan( MW_REV_DELETED_COMMENT ) ) {
+			$block = $this->commentBlock( $rev->getRawComment(), $rev->getTitle() );
+		} else {
+			$block = " <span class='comment'>" .
+				wfMsgHtml( 'rev-deleted-comment' ) . "</span>";
+		}
+		if( $rev->isDeleted( MW_REV_DELETED_COMMENT ) ) {
+			return " <span class='history-deleted'>$block</span>";
+		}
+		return $block;
+	}
 
 	/** @todo document */
 	function tocIndent() {
Index: trunk/phase3/includes/SpecialContributions.php
===================================================================
--- trunk/phase3/includes/SpecialContributions.php	(revision 13223)
+++ trunk/phase3/includes/SpecialContributions.php	(revision 13224)
@@ -128,7 +128,7 @@
 		$use_index = $this->dbr->useIndexClause($index);
 		$sql = "SELECT
 			page_namespace,page_title,page_is_new,page_latest,
-			rev_id,rev_timestamp,rev_comment,rev_minor_edit,rev_user_text,
+			rev_id,rev_page,rev_text_id,rev_timestamp,rev_comment,rev_minor_edit,rev_user,rev_user_text,
 			rev_deleted
 			FROM $page,$revision $use_index
 			WHERE page_id=rev_page AND $userCond $nscond $offsetQuery
@@ -358,8 +358,10 @@
 		}
 	}
 
-	$page =& Title::makeTitle( $row->page_namespace, $row->page_title );
-	$link = $sk->makeKnownLinkObj( $page, '' );
+	$rev = new Revision( $row );
+	
+	$page = Title::makeTitle( $row->page_namespace, $row->page_title );
+	$link = $sk->makeKnownLinkObj( $page );
 	$difftext = $topmarktext = '';
 	if( $row->rev_id == $row->page_latest ) {
 		$topmarktext .= '<strong>' . $messages['uctop'] . '</strong>';
@@ -379,15 +381,19 @@
 		}
 
 	}
-	if( $row->rev_deleted && !$wgUser->isAllowed( 'delete' ) ) {
+	if( $rev->userCan( MW_REV_DELETED_TEXT ) ) {
+		$difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
+	} else {
 		$difftext = '(' . $messages['diff'] . ')';
-	} else {
-		$difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
 	}
 	$histlink='('.$sk->makeKnownLinkObj( $page, $messages['hist'], 'action=history' ) . ')';
 
-	$comment = $sk->commentBlock( $row->rev_comment, $page, (bool)$row->rev_deleted );
+	$comment = $sk->revComment( $rev );
 	$d = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->rev_timestamp), true );
+	
+	if( $rev->isDeleted( MW_REV_DELETED_TEXT ) ) {
+		$d = '<span class="history-deleted">' . $d . '</span>';
+	}
 
 	if( $row->rev_minor_edit ) {
 		$mflag = '<span class="minor">' . $messages['minoreditletter'] . '</span> ';
@@ -396,8 +402,8 @@
 	}
 
 	$ret = "{$d} {$histlink} {$difftext} {$mflag} {$link} {$comment} {$topmarktext}";
-	if( $row->rev_deleted ) {
-		$ret = "<span class='deleted'>$ret</span>";
+	if( $rev->isDeleted( MW_REV_DELETED_TEXT ) ) {
+		$ret .= ' ' . wfMsgHtml( 'deletedrev' );
 	}
 	$ret = "<li>$ret</li>\n";
 	wfProfileOut( $fname );
Index: trunk/phase3/includes/Export.php
===================================================================
--- trunk/phase3/includes/Export.php	(revision 13223)
+++ trunk/phase3/includes/Export.php	(revision 13224)
@@ -1,5 +1,5 @@
 <?php
-# Copyright (C) 2003, 2005 Brion Vibber <brion@pobox.com>
+# Copyright (C) 2003, 2005, 2006 Brion Vibber <brion@pobox.com>
 # http://www.mediawiki.org/
 #
 # This program is free software; you can redistribute it and/or modify
@@ -232,7 +232,7 @@
 	 * @return string
 	 */
 	function schemaVersion() {
-		return "0.3";
+		return "0.3"; // FIXME: upgrade to 0.4 when updated XSD is ready, for the revision deletion bits
 	}
 
 	/**
@@ -360,23 +360,31 @@
 		$ts = wfTimestamp( TS_ISO_8601, $row->rev_timestamp );
 		$out .= "      " . wfElement( 'timestamp', null, $ts ) . "\n";
 
-		$out .= "      <contributor>\n";
-		if( $row->rev_user ) {
-			$out .= "        " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n";
-			$out .= "        " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n";
+		if( $row->rev_deleted & MW_REV_DELETED_USER ) {
+			$out .= "      " . wfElement( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
 		} else {
-			$out .= "        " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n";
+			$out .= "      <contributor>\n";
+			if( $row->rev_user ) {
+				$out .= "        " . wfElementClean( 'username', null, strval( $row->rev_user_text ) ) . "\n";
+				$out .= "        " . wfElement( 'id', null, strval( $row->rev_user ) ) . "\n";
+			} else {
+				$out .= "        " . wfElementClean( 'ip', null, strval( $row->rev_user_text ) ) . "\n";
+			}
+			$out .= "      </contributor>\n";
 		}
-		$out .= "      </contributor>\n";
 
 		if( $row->rev_minor_edit ) {
 			$out .=  "      <minor/>\n";
 		}
-		if( $row->rev_comment != '' ) {
+		if( $row->rev_deleted & MW_REV_DELETED_COMMENT ) {
+			$out .= "      " . wfElement( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
+		} elseif( $row->rev_comment != '' ) {
 			$out .= "      " . wfElementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
 		}
 
-		if( isset( $row->old_text ) ) {
+		if( $row->rev_deleted & MW_REV_DELETED_TEXT ) {
+			$out .= "      " . wfElement( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
+		} elseif( isset( $row->old_text ) ) {
 			// Raw text from the database may have invalid chars
 			$text = strval( Revision::getRevisionText( $row ) );
 			$out .= "      " . wfElementClean( 'text',
Index: trunk/phase3/includes/WebRequest.php
===================================================================
--- trunk/phase3/includes/WebRequest.php	(revision 13223)
+++ trunk/phase3/includes/WebRequest.php	(revision 13224)
@@ -162,6 +162,24 @@
 			return (array)$val;
 		}
 	}
+	
+	/**
+	 * Fetch an array of integers, or return $default if it's not set.
+	 * If source was scalar, will return an array with a single element.
+	 * If no source and no default, returns NULL.
+	 * If an array is returned, contents are guaranteed to be integers.
+	 *
+	 * @param string $name
+	 * @param array $default option default (or NULL)
+	 * @return array of ints
+	 */
+	function getIntArray( $name, $default = NULL ) {
+		$val = $this->getArray( $name, $default );
+		if( is_array( $val ) ) {
+			$val = array_map( 'intval', $val );
+		}
+		return $val;
+	}
 
 	/**
 	 * Fetch an integer value from the input or return $default if not set.
Index: trunk/phase3/includes/Revision.php
===================================================================
--- trunk/phase3/includes/Revision.php	(revision 13223)
+++ trunk/phase3/includes/Revision.php	(revision 13224)
@@ -8,6 +8,13 @@
 require_once( 'Database.php' );
 require_once( 'Article.php' );
 
+/** @+ */
+define( 'MW_REV_DELETED_TEXT',       1 );
+define( 'MW_REV_DELETED_COMMENT',    2 );
+define( 'MW_REV_DELETED_USER',       4 );
+define( 'MW_REV_DELETED_RESTRICTED', 8 );
+/** @- */
+
 /**
  * @package MediaWiki
  * @todo document
@@ -246,9 +253,14 @@
 			$this->mTimestamp =         $row->rev_timestamp;
 			$this->mDeleted   = intval( $row->rev_deleted );
 
-			$this->mCurrent   = ( $row->rev_id == $row->page_latest );
-			$this->mTitle     = Title::makeTitle( $row->page_namespace,
-			                                      $row->page_title );
+			if( isset( $row->page_latest ) ) {
+				$this->mCurrent   = ( $row->rev_id == $row->page_latest );
+				$this->mTitle     = Title::makeTitle( $row->page_namespace,
+				                                      $row->page_title );
+			} else {
+				$this->mCurrent = false;
+				$this->mTitle = null;
+			}
 
 			if( isset( $row->old_text ) ) {
 				$this->mText  = $this->getRevisionText( $row );
@@ -327,23 +339,62 @@
 	}
 
 	/**
+	 * Fetch revision's user id if it's available to all users
 	 * @return int
 	 */
 	function getUser() {
+		if( $this->isDeleted( MW_REV_DELETED_USER ) ) {
+			return 0;
+		} else {
+			return $this->mUser;
+		}
+	}
+
+	/**
+	 * Fetch revision's user id without regard for the current user's permissions
+	 * @return string
+	 */
+	function getRawUser() {
 		return $this->mUser;
 	}
 
 	/**
+	 * Fetch revision's username if it's available to all users
 	 * @return string
 	 */
 	function getUserText() {
+		if( $this->isDeleted( MW_REV_DELETED_USER ) ) {
+			return "";
+		} else {
+			return $this->mUserText;
+		}
+	}
+
+	/**
+	 * Fetch revision's username without regard for view restrictions
+	 * @return string
+	 */
+	function getRawUserText() {
 		return $this->mUserText;
 	}
+	
+	/**
+	 * Fetch revision comment if it's available to all users
+	 * @return string
+	 */
+	function getComment() {
+		if( $this->isDeleted( MW_REV_DELETED_COMMENT ) ) {
+			return "";
+		} else {
+			return $this->mComment;
+		}
+	}
 
 	/**
+	 * Fetch revision comment without regard for the current user's permissions
 	 * @return string
 	 */
-	function getComment() {
+	function getRawComment() {
 		return $this->mComment;
 	}
 
@@ -355,16 +406,30 @@
 	}
 
 	/**
+	 * int $field one of MW_REV_DELETED_* bitfield constants
 	 * @return bool
 	 */
-	function isDeleted() {
-		return (bool)$this->mDeleted;
+	function isDeleted( $field ) {
+		return ($this->mDeleted & $field) == $field;
 	}
 
 	/**
+	 * Fetch revision text if it's available to all users
 	 * @return string
 	 */
 	function getText() {
+		if( $this->isDeleted( MW_REV_DELETED_TEXT ) ) {
+			return "";
+		} else {
+			return $this->getRawText();
+		}
+	}
+	
+	/**
+	 * Fetch revision text without regard for view restrictions
+	 * @return string
+	 */
+	function getRawText() {
 		if( is_null( $this->mText ) ) {
 			// Revision text is immutable. Load on demand:
 			$this->mText = $this->loadText();
@@ -650,6 +715,28 @@
 		wfProfileOut( $fname );
 		return $revision;
 	}
+	
+	/**
+	 * Determine if the current user is allowed to view a particular
+	 * field of this revision, if it's marked as deleted.
+	 * @param int $field one of MW_REV_DELETED_TEXT,
+	 *                          MW_REV_DELETED_COMMENT,
+	 *                          MW_REV_DELETED_USER
+	 * @return bool
+	 */
+	function userCan( $field ) {
+		if( ( $this->mDeleted & $field ) == $field ) {
+			global $wgUser;
+			$permission = ( $this->mDeleted & MW_REV_DELETED_RESTRICTED ) == MW_REV_DELETED_RESTRICTED
+				? 'hiderevision'
+				: 'deleterevision';
+			wfDebug( "Checking for $permission due to $field match on $this->mDeleted\n" );
+			return $wgUser->isAllowed( $permission );
+		} else {
+			return true;
+		}
+	}
 
 }
+
 ?>
Index: trunk/phase3/includes/ChangesList.php
===================================================================
--- trunk/phase3/includes/ChangesList.php	(revision 13223)
+++ trunk/phase3/includes/ChangesList.php	(revision 13224)
@@ -181,8 +181,8 @@
 
 	/** Insert links to user page, user talk page and eventually a blocking link */
 	function insertUserRelatedLinks(&$s, &$rc) {
-		$s .= $this->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
-		$s .= $this->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+		$s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
+		$s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] );
 	}
 
 	/** insert a formatted comment */
@@ -203,79 +203,7 @@
 		  ( !$wgOnlySysopsCanPatrol || $wgUser->isAllowed( 'patrol' ) );
 	}
 
-	/**
-	 * Make user link (or user contributions for unregistered users)
-	 * @param int $userId
-	 * @param string $userText
-	 * @return string HTML fragment
-	 * @access private
-	 */
-	function userLink( $userId, $userText ) {
-		$encName = htmlspecialchars( $userText );
-		if( $userId == 0 ) {
-			$contribsPage = Title::makeTitle( NS_SPECIAL, 'Contributions' );
-			return $this->skin->makeKnownLinkObj( $contribsPage,
-				$encName, 'target=' . urlencode( $userText ) );
-		} else {
-			$userPage = Title::makeTitle( NS_USER, $userText );
-			return $this->skin->makeLinkObj( $userPage, $encName );
-		}
-	}
 
-	/**
-	 * @param int $userId
-	 * @param string $userText
-	 * @return string HTML fragment with talk and/or block links
-	 * @access private
-	 */
-	function userToolLinks( $userId, $userText ) {
-		global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans;
-		$talkable = !( $wgDisableAnonTalk && 0 == $userId );
-		$blockable = ( $wgSysopUserBans || 0 == $userId );
-
-		$items = array();
-		if( $talkable ) {
-			$items[] = $this->userTalkLink( $userId, $userText );
-		}
-		if( $blockable && $wgUser->isAllowed( 'block' ) ) {
-			$items[] = $this->blockLink( $userId, $userText );
-		}
-
-		if( $items ) {
-			return ' (' . implode( ' | ', $items ) . ')';
-		} else {
-			return '';
-		}
-	}
-
-	/**
-	 * @param int $userId
-	 * @param string $userText
-	 * @return string HTML fragment with user talk link
-	 * @access private
-	 */
-	function userTalkLink( $userId, $userText ) {
-		global $wgContLang;
-		$talkname = $wgContLang->getNsText( NS_TALK ); # use the shorter name
-
-		$userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
-		$userTalkLink = $this->skin->makeLinkObj( $userTalkPage, $talkname );
-		return $userTalkLink;
-	}
-
-	/**
-	 * @param int $userId
-	 * @param string $userText
-	 * @return string HTML fragment with block link
-	 * @access private
-	 */
-	function blockLink( $userId, $userText ) {
-		$blockPage = Title::makeTitle( NS_SPECIAL, 'Blockip' );
-		$blockLink = $this->skin->makeKnownLinkObj( $blockPage,
-			$this->message['blocklink'], 'ip=' . urlencode( $userText ) );
-		return $blockLink;
-	}
-
 }
 
 
@@ -429,13 +357,13 @@
 			  $curIdEq.'&diff='.$rc_this_oldid.'&oldid='.$rc_last_oldid . $rcIdQuery );
 		}
 
-		$rc->userlink = $this->userLink( $rc_user, $rc_user_text );
+		$rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text );
 
 		$rc->lastlink = $lastLink;
 		$rc->curlink  = $curLink;
 		$rc->difflink = $diffLink;
 
-		$rc->usertalklink = $this->userToolLinks( $rc_user, $rc_user_text );
+		$rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text );
 
 		# Put accumulated information into the cache, for later display
 		# Page moves go on their own line
Index: trunk/phase3/includes/DifferenceEngine.php
===================================================================
--- trunk/phase3/includes/DifferenceEngine.php	(revision 13223)
+++ trunk/phase3/includes/DifferenceEngine.php	(revision 13224)
@@ -24,8 +24,6 @@
 	var $mOldid, $mNewid, $mTitle;
 	var $mOldtitle, $mNewtitle, $mPagetitle;
 	var $mOldtext, $mNewtext;
-	var $mOldUser, $mNewUser;
-	var $mOldComment, $mNewComment;
 	var $mOldPage, $mNewPage;
 	var $mRcidMarkPatrolled;
 	var $mOldRev, $mNewRev;
@@ -153,21 +151,11 @@
 		$talk = $wgContLang->getNsText( NS_TALK );
 		$contribs = wfMsg( 'contribslink' );
 
-		$this->mOldComment = $sk->formatComment($this->mOldComment);
-		$this->mNewComment = $sk->formatComment($this->mNewComment);
-
-		$oldUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
-		$newUserLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mNewUser ), $this->mNewUser );
-		$oldUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ), $talk );
-		$newUTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mNewUser ), $talk );
-		$oldContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
-			'target=' . urlencode($this->mOldUser) );
-		$newContribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $contribs,
-			'target=' . urlencode($this->mNewUser) );
 		if ( $this->mNewRev->isCurrent() && $wgUser->isAllowed('rollback') ) {
+			$username = $this->mNewRev->getUserText();
 			$rollback = '&nbsp;&nbsp;&nbsp;<strong>[' . $sk->makeKnownLinkObj( $this->mTitle, wfMsg( 'rollbacklink' ),
-				'action=rollback&from=' . urlencode($this->mNewUser) .
-				'&token=' . urlencode( $wgUser->editToken( array( $this->mTitle->getPrefixedText(), $this->mNewUser ) ) ) ) .
+				'action=rollback&from=' . urlencode( $username ) .
+				'&token=' . urlencode( $wgUser->editToken( array( $this->mTitle->getPrefixedText(), $username ) ) ) ) .
 				']</strong>';
 		} else {
 			$rollback = '';
@@ -190,12 +178,14 @@
 				'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
 		}
 
-		$oldHeader = "<strong>{$this->mOldtitle}</strong><br />$oldUserLink " .
-			"($oldUTLink | $oldContribs)<br />" . $this->mOldComment .
-			'<br />' . $prevlink;
-		$newHeader = "<strong>{$this->mNewtitle}</strong><br />$newUserLink " .
-			"($newUTLink | $newContribs) $rollback<br />" . $this->mNewComment .
-			'<br />' . $nextlink . $patrol;
+		$oldHeader = "<strong>{$this->mOldtitle}</strong><br />" .
+			$sk->revUserTools( $this->mOldRev ) . "<br />" .
+			$sk->revComment( $this->mOldRev ) . "<br />" .
+			$prevlink;
+		$newHeader = "<strong>{$this->mNewtitle}</strong><br />" .
+			$sk->revUserTools( $this->mNewRev ) . " $rollback<br />" .
+			$sk->revComment( $this->mNewRev ) . "<br />" .
+			$nextlink . $patrol;
 
 		$this->showDiff( $oldHeader, $newHeader );
 		$wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
@@ -255,19 +245,16 @@
 		#
 		$sk = $wgUser->getSkin();
 
-		$uTLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER_TALK, $this->mOldUser ),  $wgLang->getNsText( NS_TALK ) );
-		$userLink = $sk->makeLinkObj( Title::makeTitleSafe( NS_USER, $this->mOldUser ), $this->mOldUser );
-		$contribs = $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), wfMsg( 'contribslink' ),
-			'target=' . urlencode($this->mOldUser) );
 		$nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
-		$header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />$userLink " .
-			"($uTLink | $contribs)<br />" . $this->mOldComment .
-			'<br />' . $nextlink. "</div>\n";
+		$header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
+			$sk->revUserTools( $this->mNewRev ) . "<br />" .
+			$sk->revComment( $this->mNewRev ) . "<br />" .
+			$nextlink . "</div>\n";
 
 		$wgOut->addHTML( $header );
 
 		$wgOut->setSubtitle( wfMsg( 'difference' ) );
-		$wgOut->setRobotpolicy( 'noindex,follow' );
+		$wgOut->setRobotpolicy( 'noindex,nofollow' );
 
 
 		# Show current revision
@@ -289,7 +276,7 @@
 		global $wgOut;
 		$diff = $this->getDiff( $otitle, $ntitle );
 		if ( $diff === false ) {
-			$wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>$t</nowiki>" ) );
+			$wgOut->addWikitext( wfMsg( 'missingarticle', "<nowiki>(fixme, bug)</nowiki>" ) );
 			return false;
 		} else {
 			$wgOut->addHTML( $diff );
@@ -510,9 +497,6 @@
 			$this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
 		}
 
-		$this->mNewUser = $this->mNewRev->getUserText();
-		$this->mNewComment = $this->mNewRev->getComment();
-
 		// Load the old revision object
 		$this->mOldRev = false;
 		if( $this->mOldid ) {
@@ -539,10 +523,6 @@
 			$t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
 			$oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
 			$this->mOldtitle = "<a href='$oldLink'>" . htmlspecialchars( wfMsg( 'revisionasof', $t ) ) . '</a>';
-
-
-			$this->mOldUser = $this->mOldRev->getUserText();
-			$this->mOldComment = $this->mOldRev->getComment();
 		}
 
 		return true;
@@ -563,6 +543,7 @@
 			return false;
 		}
 		if ( $this->mOldRev ) {
+			// FIXME: permission tests
 			$this->mOldtext = $this->mOldRev->getText();
 			if ( $this->mOldtext === false ) {
 				return false;
Index: trunk/phase3/includes/RawPage.php
===================================================================
--- trunk/phase3/includes/RawPage.php	(revision 13223)
+++ trunk/phase3/includes/RawPage.php	(revision 13224)
@@ -163,7 +163,7 @@
 				if ( $rev ) {
 					$lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
 					header( "Last-modified: $lastmod" );
-					$text = $rev->isDeleted() ? '' : $rev->getText();
+					$text = $rev->getText();
 				} else
 					$text = '';
 			}
Index: trunk/phase3/includes/DefaultSettings.php
===================================================================
--- trunk/phase3/includes/DefaultSettings.php	(revision 13223)
+++ trunk/phase3/includes/DefaultSettings.php	(revision 13224)
@@ -849,6 +849,10 @@
 // Permission to change users' group assignments
 $wgGroupPermissions['bureaucrat']['userrights'] = true;
 
+// Experimental permissions, not ready for production use
+//$wgGroupPermissions['sysop']['deleterevision'] = true;
+//$wgGroupPermissions['bureaucrat']['hiderevision'] = true;
+
 /**
  * The developer group is deprecated, but can be activated if need be
  * to use the 'lockdb' and 'unlockdb' special pages. Those require
Index: trunk/phase3/includes/PageHistory.php
===================================================================
--- trunk/phase3/includes/PageHistory.php	(revision 13223)
+++ trunk/phase3/includes/PageHistory.php	(revision 13224)
@@ -222,93 +222,101 @@
 
 	/** @todo document */
 	function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
+		global $wgUser;
+		$rev = new Revision( $row );
 
-		if ( 0 == $row->rev_user ) {
-			$contribsPage =& Title::makeTitle( NS_SPECIAL, 'Contributions' );
-			$ul = $this->mSkin->makeKnownLinkObj( $contribsPage,
-				htmlspecialchars( $row->rev_user_text ),
-				'target=' . urlencode( $row->rev_user_text ) );
-		} else {
-			$userPage =& Title::makeTitle( NS_USER, $row->rev_user_text );
-			$ul = $this->mSkin->makeLinkObj( $userPage , htmlspecialchars( $row->rev_user_text ) );
-		}
+		$s = '<li>';
+		$curlink = $this->curLink( $rev, $latest );
+		$lastlink = $this->lastLink( $rev, $next, $counter );
+		$arbitrary = $this->diffButtons( $rev, $firstInList, $counter );
+		$link = $this->revLink( $rev );
+		$user = $this->mSkin->revUserLink( $rev );
 
-		$s = '<li>';
-		/* This feature is not yet used according to schema */
-		if( $row->rev_deleted ) {
-			$s .= '<span class="history-deleted">';
+		$s .= "($curlink) ($lastlink) $arbitrary";
+		
+		if( $wgUser->isAllowed( 'deleterevision' ) ) {
+			$revdel = Title::makeTitle( NS_SPECIAL, 'Revisiondelete' );
+			if( $firstInList ) {
+				// We don't currently handle well changing the top revision's settings
+				$del = wfMsgHtml( 'rev-delundel' );
+			} else {
+				$del = $this->mSkin->makeKnownLinkObj( $revdel,
+					wfMsg( 'rev-delundel' ),
+					'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) .
+					'&oldid=' . urlencode( $rev->getId() ) );
+			}
+			$s .= "(<small>$del</small>) ";
 		}
-		$curlink = $this->curLink( $row, $latest );
-		$lastlink = $this->lastLink( $row, $next, $counter );
-		$arbitrary = $this->diffButtons( $row, $firstInList, $counter );
-		$link = $this->revLink( $row );
+		
+		$s .= " $link <span class='history-user'>$user</span>";
 
-		$s .= "($curlink) ($lastlink) $arbitrary $link <span class='history-user'>$ul</span>";
-
 		if( $row->rev_minor_edit ) {
 			$s .= ' ' . wfElement( 'span', array( 'class' => 'minor' ), wfMsgHtml( 'minoreditletter') );
 		}
 
-		$s .= $this->mSkin->commentBlock( $row->rev_comment, $this->mTitle );
+		$s .= $this->mSkin->revComment( $rev );
 		if ($notificationtimestamp && ($row->rev_timestamp >= $notificationtimestamp)) {
 			$s .= ' <span class="updatedmarker">' .  wfMsgHtml( 'updatedmarker' ) . '</span>';
 		}
-		if( $row->rev_deleted ) {
-			$s .= '</span> ' . wfMsgHtml( 'deletedrev' );
+		if( $row->rev_deleted & MW_REV_DELETED_TEXT ) {
+			$s .= ' ' . wfMsgHtml( 'deletedrev' );
 		}
 		$s .= "</li>\n";
 
 		return $s;
 	}
-
+	
 	/** @todo document */
-	function revLink( $row ) {
+	function revLink( $rev ) {
 		global $wgUser, $wgLang;
-		$date = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->rev_timestamp), true );
-		if( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) {
-			return $date;
+		$date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true );
+		if( $rev->userCan( MW_REV_DELETED_TEXT ) ) {
+			$link = $this->mSkin->makeKnownLinkObj(
+				$this->mTitle, $date, "oldid=" . $rev->getId() );
 		} else {
-			return $this->mSkin->makeKnownLinkObj(
-				$this->mTitle, $date, "oldid={$row->rev_id}" );
+			$link = $date;
 		}
+		if( $rev->isDeleted( MW_REV_DELETED_TEXT ) ) {
+			return '<span class="history-deleted">' . $link . '</span>';
+		}
+		return $link;
 	}
 
 	/** @todo document */
-	function curLink( $row, $latest ) {
+	function curLink( $rev, $latest ) {
 		global $wgUser;
 		$cur = wfMsgHtml( 'cur' );
-		if( $latest
-			|| ( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) ) {
+		if( $latest || !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
 			return $cur;
 		} else {
 			return $this->mSkin->makeKnownLinkObj(
 				$this->mTitle, $cur,
 				'diff=' . $this->getLatestID() .
-				"&oldid={$row->rev_id}" );
+				"&oldid=" . $rev->getId() );
 		}
 	}
 
 	/** @todo document */
-	function lastLink( $row, $next, $counter ) {
+	function lastLink( $rev, $next, $counter ) {
 		global $wgUser;
 		$last = htmlspecialchars( wfMsg( 'last' ) );
 		if( is_null( $next ) ) {
-			if( $row->rev_timestamp == $this->getEarliestOffset() ) {
+			if( $rev->getTimestamp() == $this->getEarliestOffset() ) {
 				return $last;
 			} else {
 				// Cut off by paging; there are more behind us...
 				return $this->mSkin->makeKnownLinkObj(
 					$this->mTitle,
 					$last,
-					"diff={$row->rev_id}&oldid=prev" );
+					"diff=" . $rev->getId() . "&oldid=prev" );
 			}
-		} elseif( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) {
+		} elseif( !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
 			return $last;
 		} else {
 			return $this->mSkin->makeKnownLinkObj(
 				$this->mTitle,
 				$last,
-				"diff={$row->rev_id}&oldid={$next->rev_id}"
+				"diff=" . $rev->getId() . "&oldid={$next->rev_id}"
 				/*,
 				'',
 				'',
@@ -317,17 +325,17 @@
 	}
 
 	/** @todo document */
-	function diffButtons( $row, $firstInList, $counter ) {
+	function diffButtons( $rev, $firstInList, $counter ) {
 		global $wgUser;
 		if( $this->linesonpage > 1) {
 			$radio = array(
 				'type'  => 'radio',
-				'value' => $row->rev_id,
+				'value' => $rev->getId(),
 # do we really need to flood this on every item?
 #				'title' => wfMsgHtml( 'selectolderversionfordiff' )
 			);
 
-			if( $row->rev_deleted && !$wgUser->isAllowed( 'undelete' ) ) {
+			if( !$rev->userCan( MW_REV_DELETED_TEXT ) ) {
 				$radio['disabled'] = 'disabled';
 			}
 
@@ -447,7 +455,7 @@
 
 		$res = $dbr->select(
 			'revision',
-			array('rev_id', 'rev_user', 'rev_comment', 'rev_user_text',
+			array('rev_id', 'rev_page', 'rev_text_id', 'rev_user', 'rev_comment', 'rev_user_text',
 				'rev_timestamp', 'rev_minor_edit', 'rev_deleted'),
 			array_merge(array("rev_page=$page_id"), $offsets),
 			$fname,
Index: trunk/phase3/includes/SpecialPage.php
===================================================================
--- trunk/phase3/includes/SpecialPage.php	(revision 13223)
+++ trunk/phase3/includes/SpecialPage.php	(revision 13224)
@@ -77,7 +77,8 @@
 	'Userrights'	=> new SpecialPage( 'Userrights', 'userrights' ),
 	'MIMEsearch'    => new SpecialPage( 'MIMEsearch' ),
 	'Unwatchedpages' => new SpecialPage( 'Unwatchedpages', 'unwatchedpages' ),
-	'Listredirects' => new SpecialPage( 'Listredirects' )
+	'Listredirects' => new SpecialPage( 'Listredirects' ),
+	'Revisiondelete' => new SpecialPage( 'Revisiondelete', 'deleterevision' ),
 );
 
 if( !$wgDisableCounters ) {
Index: trunk/phase3/includes/LogPage.php
===================================================================
--- trunk/phase3/includes/LogPage.php	(revision 13223)
+++ trunk/phase3/includes/LogPage.php	(revision 13224)
@@ -166,6 +166,7 @@
 
 			'delete/delete'     => 'deletedarticle',
 			'delete/restore'    => 'undeletedarticle',
+			'delete/revision'   => 'revdelete-logentry',
 			'upload/upload'     => 'uploadedimage',
 			'upload/revert'     => 'uploadedimage',
 			'move/move'         => '1movedto2',
Index: trunk/phase3/RELEASE-NOTES
===================================================================
--- trunk/phase3/RELEASE-NOTES	(revision 13223)
+++ trunk/phase3/RELEASE-NOTES	(revision 13224)
@@ -685,7 +685,12 @@
   unnecessary hidden UI work when watch/unwatch is performed on edit
 * Fixed bogus master fallback in external storage
 * (bug 5246) Add speak:none to "hiddenStructure" class in main.css
+* Further work on rev_deleted; changed to a bitfield with several data-hiding
+  options. Not yet ready for production use; Special:Revisiondelete is
+  incomplete, and the flags are not preserved across page deletion/undeletion.
+  To try it; add the 'deleterevision' permission to a privileged group.
 
+
 === Caveats ===
 
 Some output, particularly involving user-supplied inline HTML, may not
Index: trunk/phase3/languages/Messages.php
===================================================================
--- trunk/phase3/languages/Messages.php	(revision 13223)
+++ trunk/phase3/languages/Messages.php	(revision 13224)
@@ -565,7 +565,39 @@
 'deletedrev' => '[deleted]',
 'histfirst' => 'Earliest',
 'histlast' => 'Latest',
+'rev-deleted-comment' => '(comment removed)',
+'rev-deleted-user' => '(username removed)',
+'rev-deleted-text-permission' => '<div class="mw-warning plainlinks">
+This page revision has been removed from the public archives.
+There may be details in the [{{fullurl:Special:Log/delete|page={{PAGENAMEE}}}} deletion log].
+</div>',
+'rev-deleted-text-view' => '<div class="mw-warning plainlinks">
+This page revision has been removed from the public archives.
+As an administrator on this site you can view it;
+there may be details in the [{{fullurl:Special:Log/delete|page={{PAGENAMEE}}}} deletion log].
+</div>',
+#'rev-delundel' => 'del/undel',
+'rev-delundel' => 'show/hide',
 
+# Revision deletion
+#
+'revisiondelete' => 'Delete/undelete revisions',
+'revdelete-selected' => 'Selected revision of [[:$1]]:',
+'revdelete-text' => "Deleted revisions will still appear in the page history,
+but their text contents will be inaccessible to the public.
+
+Other admins on this wiki will still be able to access the hidden content and can
+undelete it again through this same interface, unless an additional restriction
+is placed by the site operators.",
+'revdelete-legend' => 'Set revision restrictions:',
+'revdelete-hide-text' => 'Hide revision text',
+'revdelete-hide-comment' => 'Hide edit comment',
+'revdelete-hide-user' => 'Hide editor\'s username/IP',
+'revdelete-hide-restricted' => 'Apply these restrictions to sysops as well as others',
+'revdelete-log' => 'Log comment:',
+'revdelete-submit' => 'Apply to selected revision',
+'revdelete-logentry' => 'changed revision visibility for [[$1]]',
+
 # Diffs
 #
 'difference'	=> '(Difference between revisions)',
Views
Toolbox