* Further work on rev_deleted; changed to a bitfield with several data-hiding
authorBrion Vibber <brion@users.mediawiki.org>
Thu, 16 Mar 2006 19:04:25 +0000 (19:04 +0000)
committerBrion Vibber <brion@users.mediawiki.org>
Thu, 16 Mar 2006 19:04:25 +0000 (19:04 +0000)
  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.

19 files changed:
RELEASE-NOTES
includes/Article.php
includes/ChangesList.php
includes/DefaultSettings.php
includes/DifferenceEngine.php
includes/Export.php
includes/GlobalFunctions.php
includes/Linker.php
includes/LogPage.php
includes/PageHistory.php
includes/RawPage.php
includes/Revision.php
includes/SpecialContributions.php
includes/SpecialPage.php
includes/SpecialRevisiondelete.php [new file with mode: 0644]
includes/WebRequest.php
includes/XmlFunctions.php [new file with mode: 0644]
languages/Messages.php
skins/monobook/main.css

index 18b94d2..2c4c5d0 100644 (file)
@@ -685,6 +685,11 @@ fully support the editing toolbar, but was found to be too confusing.
   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 ===
 
index 18e5f8d..f39ae2e 100644 (file)
@@ -495,7 +495,10 @@ class Article {
                        }
                }
 
-               $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 @@ class Article {
                        wfProfileOut( $fname );
                        return;
                }
-
+               
                if ( empty( $oldid ) && $this->checkTouched() ) {
                        $wgOut->setETag($parserCache->getETag($this, $wgUser));
 
@@ -846,7 +849,18 @@ class Article {
 
                        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 0f814f0..97336ff 100644 (file)
@@ -181,8 +181,8 @@ class ChangesList {
 
        /** 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,78 +203,6 @@ class ChangesList {
                  ( !$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 @@ class EnhancedChangesList extends ChangesList {
                          $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 97d0617..1daf9e3 100644 (file)
@@ -849,6 +849,10 @@ $wgGroupPermissions['sysop']['autoconfirmed']   = true;
 // 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 cdd9e57..c617034 100644 (file)
@@ -24,8 +24,6 @@ class DifferenceEngine {
        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 @@ CONTROL;
                $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 @@ CONTROL;
                                '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 @@ CONTROL;
                #
                $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 @@ CONTROL;
                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 @@ CONTROL;
                        $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 @@ CONTROL;
                        $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 @@ CONTROL;
                        return false;
                }
                if ( $this->mOldRev ) {
+                       // FIXME: permission tests
                        $this->mOldtext = $this->mOldRev->getText();
                        if ( $this->mOldtext === false ) {
                                return false;
index 88ef6e3..dee2e04 100644 (file)
@@ -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 @@ class XmlDumpWriter {
         * @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 @@ class XmlDumpWriter {
                $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 4837481..0644cf2 100644 (file)
@@ -30,6 +30,7 @@ require_once( 'DatabaseFunctions.php' );
 require_once( 'UpdateClasses.php' );
 require_once( 'LogPage.php' );
 require_once( 'normal/UtfNormalUtil.php' );
+require_once( 'XmlFunctions.php' );
 
 /**
  * Compatibility functions
@@ -839,29 +840,6 @@ function wfQuotedPrintable( $string, $charset = '' ) {
        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
@@ -872,15 +850,6 @@ function wfTime() {
        return (float)$st[0] + (float)$st[1];
 }
 
-/**
- * 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 @@ function wfGetSiteNotice() {
        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.
 *
@@ -1726,50 +1601,6 @@ function wfUrlProtocols() {
        }
 }
 
-/**
- * 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 75161bb..b512c4b 100644 (file)
@@ -736,6 +736,115 @@ class Linker {
                return '<a href="'.$url.'"'.$style.'>'.$text.'</a>';
        }
 
+       /**
+        * 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
@@ -823,24 +932,38 @@ class Linker {
         *
         * @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 */
index a1ad8c9..b43bc8a 100644 (file)
@@ -166,6 +166,7 @@ class LogPage {
 
                        'delete/delete'     => 'deletedarticle',
                        'delete/restore'    => 'undeletedarticle',
+                       'delete/revision'   => 'revdelete-logentry',
                        'upload/upload'     => 'uploadedimage',
                        'upload/revert'     => 'uploadedimage',
                        'move/move'         => '1movedto2',
index ddfe444..594a547 100644 (file)
@@ -222,93 +222,101 @@ class PageHistory {
 
        /** @todo document */
        function historyLine( $row, $next, $counter = '', $notificationtimestamp = false, $latest = false, $firstInList = false ) {
-
-               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 ) );
-               }
+               global $wgUser;
+               $rev = new Revision( $row );
 
                $s = '<li>';
-               /* This feature is not yet used according to schema */
-               if( $row->rev_deleted ) {
-                       $s .= '<span class="history-deleted">';
+               $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 .= "($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 .= "($curlink) ($lastlink) $arbitrary $link <span class='history-user'>$ul</span>";
+               
+               $s .= " $link <span class='history-user'>$user</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 @@ class PageHistory {
        }
 
        /** @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 @@ class PageHistory {
 
                $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 119f0c9..d7de77c 100644 (file)
@@ -163,7 +163,7 @@ class RawPage {
                                if ( $rev ) {
                                        $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
                                        header( "Last-modified: $lastmod" );
-                                       $text = $rev->isDeleted() ? '' : $rev->getText();
+                                       $text = $rev->getText();
                                } else
                                        $text = '';
                        }
index 7908d56..d0eabfc 100644 (file)
@@ -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 @@ class Revision {
                        $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 @@ class Revision {
        }
 
        /**
+        * 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() {
-               return $this->mUserText;
+               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 getRawComment() {
                return $this->mComment;
        }
 
@@ -355,16 +406,30 @@ class Revision {
        }
 
        /**
+        * 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 @@ class Revision {
                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 34b5d2e..f627ca6 100644 (file)
@@ -128,7 +128,7 @@ class contribs_finder {
                $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 @@ function ucListEdit( $sk, $row ) {
                }
        }
 
-       $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 @@ function ucListEdit( $sk, $row ) {
                }
 
        }
-       if( $row->rev_deleted && !$wgUser->isAllowed( 'delete' ) ) {
-               $difftext = '(' . $messages['diff'] . ')';
-       } else {
+       if( $rev->userCan( MW_REV_DELETED_TEXT ) ) {
                $difftext = '(' . $sk->makeKnownLinkObj( $page, $messages['diff'], 'diff=prev&oldid='.$row->rev_id ) . ')';
+       } else {
+               $difftext = '(' . $messages['diff'] . ')';
        }
        $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 @@ function ucListEdit( $sk, $row ) {
        }
 
        $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 17388ac..eb64341 100644 (file)
@@ -77,7 +77,8 @@ $wgSpecialPages = array(
        '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 ) {
diff --git a/includes/SpecialRevisiondelete.php b/includes/SpecialRevisiondelete.php
new file mode 100644 (file)
index 0000000..51ff681
--- /dev/null
@@ -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 );
+       }
+}
+
+?>
index 03939de..a0d8b13 100644 (file)
@@ -162,6 +162,24 @@ class WebRequest {
                        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.
diff --git a/includes/XmlFunctions.php b/includes/XmlFunctions.php
new file mode 100644 (file)
index 0000000..7864803
--- /dev/null
@@ -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
index aa2a428..248d2e3 100644 (file)
@@ -565,6 +565,38 @@ Legend: (cur) = difference with current version,
 '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
 #
index 4c67d07..bea3f64 100644 (file)
@@ -341,7 +341,8 @@ table.small {
 */
 
 #toc,
-.toc {
+.toc,
+.mw-warning {
        border: 1px solid #aaa;
        background-color: #f9f9f9;
        padding: 5px;
@@ -378,6 +379,11 @@ table.small {
        font-size: 94%;
 }
 
+.mw-warning {
+       margin-left: 50px;
+       margin-right: 50px;
+       text-align: center;
+}
 
 /* images */
 div.floatright, table.floatright {