Improve API query RevDel handling
authorBrad Jorsch <bjorsch@wikimedia.org>
Wed, 18 Dec 2013 21:58:39 +0000 (16:58 -0500)
committerBrad Jorsch <bjorsch@wikimedia.org>
Fri, 17 Jan 2014 16:43:22 +0000 (11:43 -0500)
* ApiQueryDeletedrevs, ApiQueryFilearchive, ApiQueryRecentChanges, and
  ApiQueryWatchlist will now return entires where fields have been
  revision-deleted. "Hidden" indicators will be provided as appropriate.
* ApiQueryImageInfo, ApiQueryLogEvents, ApiQueryRevisions,
  ApiQueryContributions will now return field values in addition to the
  "hidden" indicators when the requesting user has the necessary rights.
* Modules that return "hidden" indicators will now also return a
  "suppressed" indicator.
* ApiQueryImageInfo will now return info for DELETED_FILE file revisions
  if the requesting user has the 'deletedtext' right.
* ApiQueryLogEvents, when searching by user or title, will now return
  entries where the user or action are revision-deleted if the
  requesting user has the 'deletedhistory' right.
* ApiQueryContributions now uses the correct user rights rather than
  'hideuser' to determine when to show contributions where the username
  was revision-deleted.
* ApiQueryContributions will now indicate when the revision text is
  hidden.
* Fix a bug in ApiQueryDeletedrevs found during testing where specifying
  the "content" prop along with the "tags" prop or "drtag" parameter
  would cause an SQL error.
* Fix various PHP warnings in ApiQueryFilearchive caused by the lack of
  ArchivedFile::selectFields() fields.
* ApiQueryImageInfo::getInfo's $metadataOpts parameter has been renamed
  $opts, and now may have an option to indicate the user to use for
  RevDel visibility checks.
* ApiQueryWatchlist now properly uses the actual user's rights for
  checking whether wlprop=patrol is allowed, rather than using the
  wlowner's rights.

Bug: 27747
Bug: 27748
Bug: 28261
Bug: 34926
Bug: 48966
Change-Id: Idec2199976f460e1c73a26d0717e9fc4ab8042bb

RELEASE-NOTES-1.23
includes/api/ApiQueryBase.php
includes/api/ApiQueryDeletedrevs.php
includes/api/ApiQueryFilearchive.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiQueryLogEvents.php
includes/api/ApiQueryRecentChanges.php
includes/api/ApiQueryRevisions.php
includes/api/ApiQueryUserContributions.php
includes/api/ApiQueryWatchlist.php

index 605e4ec..a5e223b 100644 (file)
@@ -125,6 +125,24 @@ production.
 * All API modules now support an assert parameter. See the new features section
   for more details.
 * Added prop=contributors to fetch the list of contributors to the page.
+* The following API modules will now return entries where fields have been
+  revision-deleted: list=deletedrevs, list=filearchive, list=recentchanges,
+  list=watchlist. "hidden" indicators will be included, in the same style as is
+  already done for prop=revisions.
+* The following API modules will now return the content of revision-deleted
+  fields, in addition to the "hidden" indicators, if the querying user has the
+  necessary rights: list=logevents, list=usercontribs, prop=imageinfo,
+  prop=revisions.
+* The above modules, where applicable, will now return entries filtered by
+  revision-deleted fields if the querying user has the necessary rights. For
+  example, prop=revisions with rvuser or rvexcludeuser will no longer skip
+  revisions where the user was revision-deleted if the current user has the
+  deletedhistory right.
+* The 'hideuser' right, used when blocking, is no longer necessary or
+  sufficient for seeing contributions with revision-deleted in
+  list=usercontribs.
+* list=watchlist now uses the querying user's rights rather than the wlowner's
+  rights when checking whether wlprop=patrol is allowed.
 
 === Languages updated in 1.23 ===
 
index 32a03f1..eb51095 100644 (file)
@@ -608,6 +608,15 @@ abstract class ApiQueryBase extends ApiBase {
 
                return $errors;
        }
+
+       /**
+        * Check whether the current user has permission to view revision-deleted
+        * fields.
+        * @return bool
+        */
+       public function userCanSeeRevDel() {
+               return $this->getUser()->isAllowedAny( 'deletedhistory', 'deletedtext', 'suppressrevision' );
+       }
 }
 
 /**
index 4e4b2cc..7585ba7 100644 (file)
@@ -66,6 +66,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        $fld_token = false;
                }
 
+               // If user can't undelete, no tokens
+               if ( !$user->isAllowed( 'undelete' ) ) {
+                       $fld_token = false;
+               }
+
                $result = $this->getResult();
                $pageSet = $this->getPageSet();
                $titles = $pageSet->getTitles();
@@ -101,8 +106,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                }
 
                $this->addTables( 'archive' );
-               $this->addWhere( 'ar_deleted = 0' );
-               $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp' ) );
+               $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_deleted' ) );
 
                $this->addFieldsIf( 'ar_parent_id', $fld_parentid );
                $this->addFieldsIf( 'ar_rev_id', $fld_revid );
@@ -131,11 +135,13 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
 
                if ( $fld_content ) {
                        $this->addTables( 'text' );
+                       $this->addJoinConds(
+                               array( 'text' => array( 'INNER JOIN', array( 'ar_text_id=old_id' ) ) )
+                       );
                        $this->addFields( array( 'ar_text', 'ar_text_id', 'old_text', 'old_flags' ) );
-                       $this->addWhere( 'ar_text_id = old_id' );
 
                        // This also means stricter restrictions
-                       if ( !$user->isAllowed( 'undelete' ) ) {
+                       if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) {
                                $this->dieUsage(
                                        'You don\'t have permission to view deleted revision content',
                                        'permissiondenied'
@@ -188,6 +194,22 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                $db->addQuotes( $params['excludeuser'] ) );
                }
 
+               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
+                       // Paranoia: avoid brute force searches (bug 17342)
+                       // (shouldn't be able to get here without 'deletedhistory', but
+                       // check it again just in case)
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = Revision::DELETED_USER;
+                       } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" );
+                       }
+               }
+
                if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) {
                        $cont = explode( '|', $params['continue'] );
                        $this->dieContinueUsageIf( count( $cont ) != 3 );
@@ -243,6 +265,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        }
 
                        $rev = array();
+                       $anyHidden = false;
+
                        $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp );
                        if ( $fld_revid ) {
                                $rev['revid'] = intval( $row->ar_rev_id );
@@ -250,21 +274,37 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                        if ( $fld_parentid && !is_null( $row->ar_parent_id ) ) {
                                $rev['parentid'] = intval( $row->ar_parent_id );
                        }
-                       if ( $fld_user ) {
-                               $rev['user'] = $row->ar_user_text;
-                       }
-                       if ( $fld_userid ) {
-                               $rev['userid'] = $row->ar_user;
-                       }
-                       if ( $fld_comment ) {
-                               $rev['comment'] = $row->ar_comment;
+                       if ( $fld_user || $fld_userid ) {
+                               if ( $row->ar_deleted & Revision::DELETED_USER ) {
+                                       $rev['userhidden'] = '';
+                                       $anyHidden = true;
+                               }
+                               if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_USER, $user ) ) {
+                                       if ( $fld_user ) {
+                                               $rev['user'] = $row->ar_user_text;
+                                       }
+                                       if ( $fld_userid ) {
+                                               $rev['userid'] = $row->ar_user;
+                                       }
+                               }
                        }
 
-                       $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
-
-                       if ( $fld_parsedcomment ) {
-                               $rev['parsedcomment'] = Linker::formatComment( $row->ar_comment, $title );
+                       if ( $fld_comment || $fld_parsedcomment ) {
+                               if ( $row->ar_deleted & Revision::DELETED_COMMENT ) {
+                                       $rev['commenthidden'] = '';
+                                       $anyHidden = true;
+                               }
+                               if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_COMMENT, $user ) ) {
+                                       if ( $fld_comment ) {
+                                               $rev['comment'] = $row->ar_comment;
+                                       }
+                                       if ( $fld_parsedcomment ) {
+                                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
+                                               $rev['parsedcomment'] = Linker::formatComment( $row->ar_comment, $title );
+                                       }
+                               }
                        }
+
                        if ( $fld_minor && $row->ar_minor_edit == 1 ) {
                                $rev['minor'] = '';
                        }
@@ -272,14 +312,26 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                $rev['len'] = $row->ar_len;
                        }
                        if ( $fld_sha1 ) {
-                               if ( $row->ar_sha1 != '' ) {
-                                       $rev['sha1'] = wfBaseConvert( $row->ar_sha1, 36, 16, 40 );
-                               } else {
-                                       $rev['sha1'] = '';
+                               if ( $row->ar_deleted & Revision::DELETED_TEXT ) {
+                                       $rev['sha1hidden'] = '';
+                                       $anyHidden = true;
+                               }
+                               if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
+                                       if ( $row->ar_sha1 != '' ) {
+                                               $rev['sha1'] = wfBaseConvert( $row->ar_sha1, 36, 16, 40 );
+                                       } else {
+                                               $rev['sha1'] = '';
+                                       }
                                }
                        }
                        if ( $fld_content ) {
-                               ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                               if ( $row->ar_deleted & Revision::DELETED_TEXT ) {
+                                       $rev['texthidden'] = '';
+                                       $anyHidden = true;
+                               }
+                               if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) {
+                                       ApiResult::setContent( $rev, Revision::getRevisionText( $row ) );
+                               }
                        }
 
                        if ( $fld_tags ) {
@@ -292,11 +344,16 @@ class ApiQueryDeletedrevs extends ApiQueryBase {
                                }
                        }
 
+                       if ( $anyHidden && ( $row->ar_deleted & Revision::DELETED_RESTRICTED ) ) {
+                               $rev['suppressed'] = '';
+                       }
+
                        if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) {
                                $pageID = $newPageID++;
                                $pageMap[$row->ar_namespace][$row->ar_title] = $pageID;
                                $a['revisions'] = array( $rev );
                                $result->setIndexedTagName( $a['revisions'], 'rev' );
+                               $title = Title::makeTitle( $row->ar_namespace, $row->ar_title );
                                ApiQueryBase::addTitleInfo( $a, $title );
                                if ( $fld_token ) {
                                        $a['token'] = $token;
index e2c9964..bcbc642 100644 (file)
@@ -66,6 +66,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
 
                $this->addTables( 'filearchive' );
 
+               $this->addFields( ArchivedFile::selectFields() );
                $this->addFields( array( 'fa_name', 'fa_deleted' ) );
                $this->addFieldsIf( 'fa_sha1', $fld_sha1 );
                $this->addFieldsIf( 'fa_timestamp', $fld_timestamp );
@@ -121,14 +122,16 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        }
                }
 
-               if ( !$user->isAllowed( 'suppressrevision' ) ) {
-                       // Filter out revisions that the user is not allowed to see. There
-                       // is no way to indicate that we have skipped stuff because the
-                       // continuation parameter is fa_name
-
-                       // Note that this field is unindexed. This should however not be
-                       // a big problem as files with fa_deleted are rare
-                       $this->addWhereFld( 'fa_deleted', 0 );
+               // Exclude files this user can't view.
+               if ( !$user->isAllowed( 'deletedtext' ) ) {
+                       $bitmask = File::DELETED_FILE;
+               } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                       $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED;
+               } else {
+                       $bitmask = 0;
+               }
+               if ( $bitmask ) {
+                       $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" );
                }
 
                $limit = $params['limit'];
@@ -153,16 +156,27 @@ class ApiQueryFilearchive extends ApiQueryBase {
                        $title = Title::makeTitle( NS_FILE, $row->fa_name );
                        self::addTitleInfo( $file, $title );
 
+                       if ( $fld_description &&
+                               Revision::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user )
+                       ) {
+                               $file['description'] = $row->fa_description;
+                               if ( isset( $prop['parseddescription'] ) ) {
+                                       $file['parseddescription'] = Linker::formatComment(
+                                               $row->fa_description, $title );
+                               }
+                       }
+                       if ( $fld_user &&
+                               Revision::userCanBitfield( $row->fa_deleted, File::DELETED_USER, $user )
+                       ) {
+                               $file['userid'] = $row->fa_user;
+                               $file['user'] = $row->fa_user_text;
+                       }
                        if ( $fld_sha1 ) {
                                $file['sha1'] = wfBaseConvert( $row->fa_sha1, 36, 16, 40 );
                        }
                        if ( $fld_timestamp ) {
                                $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp );
                        }
-                       if ( $fld_user ) {
-                               $file['userid'] = $row->fa_user;
-                               $file['user'] = $row->fa_user_text;
-                       }
                        if ( $fld_size || $fld_dimensions ) {
                                $file['size'] = $row->fa_size;
 
@@ -174,13 +188,6 @@ class ApiQueryFilearchive extends ApiQueryBase {
                                $file['height'] = $row->fa_height;
                                $file['width'] = $row->fa_width;
                        }
-                       if ( $fld_description ) {
-                               $file['description'] = $row->fa_description;
-                               if ( isset( $prop['parseddescription'] ) ) {
-                                       $file['parseddescription'] = Linker::formatComment(
-                                               $row->fa_description, $title );
-                               }
-                       }
                        if ( $fld_mediatype ) {
                                $file['mediatype'] = $row->fa_media_type;
                        }
index bf11a76..cfc858a 100644 (file)
@@ -50,11 +50,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
 
                $scale = $this->getScale( $params );
 
-               $metadataOpts = array(
+               $opts = array(
                        'version' => $params['metadataversion'],
                        'language' => $params['extmetadatalanguage'],
                        'multilang' => $params['extmetadatamultilang'],
                        'extmetadatafilter' => $params['extmetadatafilter'],
+                       'revdelUser' => $this->getUser(),
                );
 
                $pageIds = $this->getPageSet()->getAllTitlesByNamespace();
@@ -78,13 +79,21 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                }
                        }
 
-                       $result = $this->getResult();
-                       //search only inside the local repo
+                       $user = $this->getUser();
+                       $findTitles = array_map( function ( $title ) use ( $user ) {
+                               return array(
+                                       'title' => $title,
+                                       'private' => $user,
+                               );
+                       }, $titles );
+
                        if ( $params['localonly'] ) {
-                               $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles );
+                               $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $findTitles );
                        } else {
-                               $images = RepoGroup::singleton()->findFiles( $titles );
+                               $images = RepoGroup::singleton()->findFiles( $findTitles );
                        }
+
+                       $result = $this->getResult();
                        foreach ( $titles as $title ) {
                                $pageId = $pageIds[NS_FILE][$title];
                                $start = $title === $fromTitle ? $fromTimestamp : $params['start'];
@@ -154,7 +163,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
 
                                        $fit = $this->addPageSubItem( $pageId,
                                                self::getInfo( $img, $prop, $result,
-                                                       $finalThumbParams, $metadataOpts
+                                                       $finalThumbParams, $opts
                                                )
                                        );
                                        if ( !$fit ) {
@@ -189,7 +198,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                        $fit = self::getTransformCount() < self::TRANSFORM_LIMIT &&
                                                $this->addPageSubItem( $pageId,
                                                        self::getInfo( $oldie, $prop, $result,
-                                                               $finalThumbParams, $metadataOpts
+                                                               $finalThumbParams, $opts
                                                        )
                                                );
                                        if ( !$fit ) {
@@ -310,25 +319,29 @@ class ApiQueryImageInfo extends ApiQueryBase {
         * @param array $prop of properties to get (in the keys)
         * @param $result ApiResult object
         * @param array $thumbParams containing 'width' and 'height' items, or null
-        * @param array|bool|string $metadataOpts Options for metadata fetching.
+        * @param array|bool|string $opts Options for data fetching.
         *   This is an array consisting of the keys:
         *    'version': The metadata version for the metadata option
         *    'language': The language for extmetadata property
         *    'multilang': Return all translations in extmetadata property
+        *    'revdelUser': User to use when checking whether to show revision-deleted fields.
         * @return Array: result array
         */
-       static function getInfo( $file, $prop, $result, $thumbParams = null, $metadataOpts = false ) {
+       static function getInfo( $file, $prop, $result, $thumbParams = null, $opts = false ) {
                global $wgContLang;
 
-               if ( !$metadataOpts || is_string( $metadataOpts ) ) {
-                       $metadataOpts = array(
-                               'version' => $metadataOpts ?: 'latest',
+               $anyHidden = false;
+
+               if ( !$opts || is_string( $opts ) ) {
+                       $opts = array(
+                               'version' => $opts ?: 'latest',
                                'language' => $wgContLang,
                                'multilang' => false,
                                'extmetadatafilter' => array(),
+                               'revdelUser' => null,
                        );
                }
-               $version = $metadataOpts['version'];
+               $version = $opts['version'];
                $vals = array();
                // Timestamp is shown even if the file is revdelete'd in interface
                // so do same here.
@@ -336,13 +349,27 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
                }
 
+               // Handle external callers who don't pass revdelUser
+               if ( isset( $opts['revdelUser'] ) && $opts['revdelUser'] ) {
+                       $revdelUser = $opts['revdelUser'];
+                       $canShowField = function ( $field ) use ( $file, $revdelUser ) {
+                               return $file->userCan( $field, $revdelUser );
+                       };
+               } else {
+                       $canShowField = function ( $field ) use ( $file ) {
+                               return !$file->isDeleted( $field );
+                       };
+               }
+
                $user = isset( $prop['user'] );
                $userid = isset( $prop['userid'] );
 
                if ( $user || $userid ) {
                        if ( $file->isDeleted( File::DELETED_USER ) ) {
                                $vals['userhidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( $canShowField( File::DELETED_USER ) ) {
                                if ( $user ) {
                                        $vals['user'] = $file->getUser();
                                }
@@ -374,13 +401,15 @@ class ApiQueryImageInfo extends ApiQueryBase {
                if ( $pcomment || $comment ) {
                        if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( $canShowField( File::DELETED_COMMENT ) ) {
                                if ( $pcomment ) {
                                        $vals['parsedcomment'] = Linker::formatComment(
-                                               $file->getDescription(), $file->getTitle() );
+                                               $file->getDescription( File::RAW ), $file->getTitle() );
                                }
                                if ( $comment ) {
-                                       $vals['comment'] = $file->getDescription();
+                                       $vals['comment'] = $file->getDescription( File::RAW );
                                }
                        }
                }
@@ -396,11 +425,20 @@ class ApiQueryImageInfo extends ApiQueryBase {
                $bitdepth = isset( $prop['bitdepth'] );
                $uploadwarning = isset( $prop['uploadwarning'] );
 
-               if ( ( $canonicaltitle || $url || $sha1 || $meta || $mime || $mediatype || $archive || $bitdepth )
-                       && $file->isDeleted( File::DELETED_FILE )
-               ) {
+               if ( $uploadwarning ) {
+                       $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) );
+               }
+
+               if ( $file->isDeleted( File::DELETED_FILE ) ) {
                        $vals['filehidden'] = '';
+                       $anyHidden = true;
+               }
+
+               if ( $anyHidden && $file->isDeleted( File::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = true;
+               }
 
+               if ( !$canShowField( File::DELETED_FILE ) ) {
                        //Early return, tidier than indenting all following things one level
                        return $vals;
                }
@@ -458,12 +496,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        // start with a letter, and all the values are strings.
                        // Thus there should be no issue with format=xml.
                        $format = new FormatMetadata;
-                       $format->setSingleLanguage( !$metadataOpts['multilang'] );
-                       $format->getContext()->setLanguage( $metadataOpts['language'] );
+                       $format->setSingleLanguage( !$opts['multilang'] );
+                       $format->getContext()->setLanguage( $opts['language'] );
                        $extmetaArray = $format->fetchExtendedMetadata( $file );
-                       if ( $metadataOpts['extmetadatafilter'] ) {
+                       if ( $opts['extmetadatafilter'] ) {
                                $extmetaArray = array_intersect_key(
-                                       $extmetaArray, array_flip( $metadataOpts['extmetadatafilter'] )
+                                       $extmetaArray, array_flip( $opts['extmetadatafilter'] )
                                );
                        }
                        $vals['extmetadata'] = $extmetaArray;
@@ -485,10 +523,6 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        $vals['bitdepth'] = $file->getBitDepth();
                }
 
-               if ( $uploadwarning ) {
-                       $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) );
-               }
-
                return $vals;
        }
 
@@ -528,6 +562,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
        }
 
        public function getCacheMode( $params ) {
+               if ( $this->userCanSeeRevDel() ) {
+                       return 'private';
+               }
+
                return 'public';
        }
 
index e4ce256..1578775 100644 (file)
@@ -158,11 +158,23 @@ class ApiQueryLogEvents extends ApiQueryBase {
                }
 
                // Paranoia: avoid brute force searches (bug 17342)
-               if ( !is_null( $title ) ) {
-                       $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0' );
-               }
-               if ( !is_null( $user ) ) {
-                       $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0' );
+               if ( !is_null( $title ) || !is_null( $user ) ) {
+                       if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) {
+                               $titleBits = LogPage::DELETED_ACTION;
+                               $userBits = LogPage::DELETED_USER;
+                       } elseif ( !$this->getUser()->isAllowed( 'suppressrevision' ) ) {
+                               $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+                               $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED;
+                       } else {
+                               $titleBits = 0;
+                               $userBits = 0;
+                       }
+                       if ( !is_null( $title ) && $titleBits ) {
+                               $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" );
+                       }
+                       if ( !is_null( $user ) && $userBits ) {
+                               $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" );
+                       }
                }
 
                $count = 0;
@@ -296,6 +308,8 @@ class ApiQueryLogEvents extends ApiQueryBase {
        private function extractRowInfo( $row ) {
                $logEntry = DatabaseLogEntry::newFromRow( $row );
                $vals = array();
+               $anyHidden = false;
+               $user = $this->getUser();
 
                if ( $this->fld_ids ) {
                        $vals['logid'] = intval( $row->log_id );
@@ -305,16 +319,29 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        $title = Title::makeTitle( $row->log_namespace, $row->log_title );
                }
 
-               if ( $this->fld_title || $this->fld_ids ) {
+               if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) {
                        if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
                                $vals['actionhidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) {
                                if ( $this->fld_title ) {
                                        ApiQueryBase::addTitleInfo( $vals, $title );
                                }
                                if ( $this->fld_ids ) {
                                        $vals['pageid'] = intval( $row->page_id );
                                }
+                               if ( $this->fld_details && $row->log_params !== '' ) {
+                                       self::addLogParams(
+                                               $this->getResult(),
+                                               $vals,
+                                               $logEntry->getParameters(),
+                                               $logEntry->getType(),
+                                               $logEntry->getSubtype(),
+                                               $logEntry->getTimestamp(),
+                                               $logEntry->isLegacy()
+                                       );
+                               }
                        }
                }
 
@@ -323,26 +350,12 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        $vals['action'] = $row->log_action;
                }
 
-               if ( $this->fld_details && $row->log_params !== '' ) {
-                       if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) {
-                               $vals['actionhidden'] = '';
-                       } else {
-                               self::addLogParams(
-                                       $this->getResult(),
-                                       $vals,
-                                       $logEntry->getParameters(),
-                                       $logEntry->getType(),
-                                       $logEntry->getSubtype(),
-                                       $logEntry->getTimestamp(),
-                                       $logEntry->isLegacy()
-                               );
-                       }
-               }
-
                if ( $this->fld_user || $this->fld_userid ) {
                        if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) {
                                $vals['userhidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) {
                                if ( $this->fld_user ) {
                                        $vals['user'] = $row->user_name === null ? $row->log_user_text : $row->user_name;
                                }
@@ -362,7 +375,9 @@ class ApiQueryLogEvents extends ApiQueryBase {
                if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->log_comment ) ) {
                        if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) {
                                if ( $this->fld_comment ) {
                                        $vals['comment'] = $row->log_comment;
                                }
@@ -383,10 +398,17 @@ class ApiQueryLogEvents extends ApiQueryBase {
                        }
                }
 
+               if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = '';
+               }
+
                return $vals;
        }
 
        public function getCacheMode( $params ) {
+               if ( $this->userCanSeeRevDel() ) {
+                       return 'private';
+               }
                if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
                        // formatComment() calls wfMessage() among other things
                        return 'anon-public-user-private';
index b298003..6ccc288 100644 (file)
@@ -145,7 +145,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                /* Build our basic query. Namely, something along the lines of:
                 * SELECT * FROM recentchanges WHERE rc_timestamp > $start
                 *              AND rc_timestamp < $end AND rc_namespace = $namespace
-                *              AND rc_deleted = 0
                 */
                $this->addTables( 'recentchanges' );
                $index = array( 'recentchanges' => 'rc_timestamp' ); // May change
@@ -176,7 +175,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                ) );
 
                $this->addWhereFld( 'rc_namespace', $params['namespace'] );
-               $this->addWhereFld( 'rc_deleted', 0 );
 
                if ( !is_null( $params['type'] ) ) {
                        $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) );
@@ -326,6 +324,36 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        $this->addWhereFld( 'ct_tag', $params['tag'] );
                }
 
+               // Paranoia: avoid brute force searches (bug 17342)
+               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = Revision::DELETED_USER;
+                       } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
+                       }
+               }
+               if ( $this->getRequest()->getCheck( 'namespace' ) ) {
+                       // LogPage::DELETED_ACTION hides the affected page, too.
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = LogPage::DELETED_ACTION;
+                       } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                               $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $this->getDB()->makeList( array(
+                                       'rc_type != ' . RC_LOG,
+                                       $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
+                               ), LIST_OR ) );
+                       }
+               }
+
                $this->token = $params['token'];
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
                $this->addOption( 'USE INDEX', $index );
@@ -389,6 +417,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
        public function extractRowInfo( $row ) {
                /* Determine the title of the page that has been changed. */
                $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+               $user = $this->getUser();
 
                /* Our output data. */
                $vals = array();
@@ -419,32 +448,50 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                                $vals['type'] = $type;
                }
 
+               $anyHidden = false;
+
                /* Create a new entry in the result for the title. */
-               if ( $this->fld_title ) {
-                       ApiQueryBase::addTitleInfo( $vals, $title );
+               if ( $this->fld_title || $this->fld_ids ) {
+                       if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
+                               $vals['actionhidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $type !== RC_LOG ||
+                               LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
+                       ) {
+                               if ( $this->fld_title ) {
+                                       ApiQueryBase::addTitleInfo( $vals, $title );
+                               }
+                               if ( $this->fld_ids ) {
+                                       $vals['pageid'] = intval( $row->rc_cur_id );
+                                       $vals['revid'] = intval( $row->rc_this_oldid );
+                                       $vals['old_revid'] = intval( $row->rc_last_oldid );
+                               }
+                       }
                }
 
-               /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */
                if ( $this->fld_ids ) {
                        $vals['rcid'] = intval( $row->rc_id );
-                       $vals['pageid'] = intval( $row->rc_cur_id );
-                       $vals['revid'] = intval( $row->rc_this_oldid );
-                       $vals['old_revid'] = intval( $row->rc_last_oldid );
                }
 
-               /* Add user data and 'anon' flag, if use is anonymous. */
+               /* Add user data and 'anon' flag, if user is anonymous. */
                if ( $this->fld_user || $this->fld_userid ) {
-
-                       if ( $this->fld_user ) {
-                               $vals['user'] = $row->rc_user_text;
+                       if ( $row->rc_deleted & Revision::DELETED_USER ) {
+                               $vals['userhidden'] = '';
+                               $anyHidden = true;
                        }
+                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) {
+                               if ( $this->fld_user ) {
+                                       $vals['user'] = $row->rc_user_text;
+                               }
 
-                       if ( $this->fld_userid ) {
-                               $vals['userid'] = $row->rc_user;
-                       }
+                               if ( $this->fld_userid ) {
+                                       $vals['userid'] = $row->rc_user;
+                               }
 
-                       if ( !$row->rc_user ) {
-                               $vals['anon'] = '';
+                               if ( !$row->rc_user ) {
+                                       $vals['anon'] = '';
+                               }
                        }
                }
 
@@ -473,12 +520,20 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                }
 
                /* Add edit summary / log summary. */
-               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
-                       $vals['comment'] = $row->rc_comment;
-               }
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       if ( $row->rc_deleted & Revision::DELETED_COMMENT ) {
+                               $vals['commenthidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
+                               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
+                                       $vals['comment'] = $row->rc_comment;
+                               }
 
-               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
-                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+                               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+                               }
+                       }
                }
 
                if ( $this->fld_redirect ) {
@@ -492,23 +547,29 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        $vals['patrolled'] = '';
                }
 
-               if ( $this->fld_patrolled && ChangesList::isUnpatrolled( $row, $this->getUser() ) ) {
+               if ( $this->fld_patrolled && ChangesList::isUnpatrolled( $row, $user ) ) {
                        $vals['unpatrolled'] = '';
                }
 
                if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
-                       $vals['logid'] = intval( $row->rc_logid );
-                       $vals['logtype'] = $row->rc_log_type;
-                       $vals['logaction'] = $row->rc_log_action;
-                       $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
-                       ApiQueryLogEvents::addLogParams(
-                               $this->getResult(),
-                               $vals,
-                               $logEntry->getParameters(),
-                               $logEntry->getType(),
-                               $logEntry->getSubtype(),
-                               $logEntry->getTimestamp()
-                       );
+                       if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
+                               $vals['actionhidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
+                               $vals['logid'] = intval( $row->rc_logid );
+                               $vals['logtype'] = $row->rc_log_type;
+                               $vals['logaction'] = $row->rc_log_action;
+                               $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
+                               ApiQueryLogEvents::addLogParams(
+                                       $this->getResult(),
+                                       $vals,
+                                       $logEntry->getParameters(),
+                                       $logEntry->getType(),
+                                       $logEntry->getSubtype(),
+                                       $logEntry->getTimestamp()
+                               );
+                       }
                }
 
                if ( $this->fld_tags ) {
@@ -522,15 +583,16 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                }
 
                if ( $this->fld_sha1 && $row->rev_sha1 !== null ) {
-                       // The RevDel check should currently never pass due to the
-                       // rc_deleted = 0 condition in the WHERE clause, but in case that
-                       // ever changes we check it here too.
                        if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                                $vals['sha1hidden'] = '';
-                       } elseif ( $row->rev_sha1 !== '' ) {
-                               $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 );
-                       } else {
-                               $vals['sha1'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( Revision::userCanBitfield( $row->rev_deleted, Revision::DELETED_TEXT, $user ) ) {
+                               if ( $row->rev_sha1 !== '' ) {
+                                       $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 );
+                               } else {
+                                       $vals['sha1'] = '';
+                               }
                        }
                }
 
@@ -547,6 +609,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                        }
                }
 
+               if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = '';
+               }
+
                return $vals;
        }
 
@@ -585,6 +651,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
                if ( isset( $params['token'] ) ) {
                        return 'private';
                }
+               if ( $this->userCanSeeRevDel() ) {
+                       return 'private';
+               }
                if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
                        // formatComment() calls wfMessage() among other things
                        return 'anon-public-user-private';
index ea992b3..bd5c512 100644 (file)
@@ -147,7 +147,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                if ( !$difftoRev ) {
                                        $this->dieUsageMsg( array( 'nosuchrevid', $params['diffto'] ) );
                                }
-                               if ( $difftoRev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               if ( !$diffToRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
                                        $this->setWarning( "Couldn't diff to r{$difftoRev->getID()}: content is hidden" );
                                        $params['diffto'] = null;
                                }
@@ -316,7 +316,16 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                        if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
                                // Paranoia: avoid brute force searches (bug 17342)
-                               $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
+                               if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) {
+                                       $bitmask = Revision::DELETED_USER;
+                               } elseif ( !$this->getUser()->isAllowed( 'suppressrevision' ) ) {
+                                       $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                               } else {
+                                       $bitmask = 0;
+                               }
+                               if ( $bitmask ) {
+                                       $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
+                               }
                        }
                } elseif ( $revCount > 0 ) {
                        $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax;
@@ -408,7 +417,9 @@ class ApiQueryRevisions extends ApiQueryBase {
        private function extractRowInfo( $row ) {
                $revision = new Revision( $row );
                $title = $revision->getTitle();
+               $user = $this->getUser();
                $vals = array();
+               $anyHidden = false;
 
                if ( $this->fld_ids ) {
                        $vals['revid'] = intval( $revision->getId() );
@@ -425,11 +436,13 @@ class ApiQueryRevisions extends ApiQueryBase {
                if ( $this->fld_user || $this->fld_userid ) {
                        if ( $revision->isDeleted( Revision::DELETED_USER ) ) {
                                $vals['userhidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_USER, $user ) ) {
                                if ( $this->fld_user ) {
-                                       $vals['user'] = $revision->getUserText();
+                                       $vals['user'] = $revision->getRawUserText();
                                }
-                               $userid = $revision->getUser();
+                               $userid = $revision->getRawUser();
                                if ( !$userid ) {
                                        $vals['anon'] = '';
                                }
@@ -452,14 +465,18 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                }
 
-               if ( $this->fld_sha1 && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                       if ( $revision->getSha1() != '' ) {
-                               $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 );
-                       } else {
-                               $vals['sha1'] = '';
+               if ( $this->fld_sha1 ) {
+                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $vals['sha1hidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) {
+                               if ( $revision->getSha1() != '' ) {
+                                       $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 );
+                               } else {
+                                       $vals['sha1'] = '';
+                               }
                        }
-               } elseif ( $this->fld_sha1 ) {
-                       $vals['sha1hidden'] = '';
                }
 
                if ( $this->fld_contentmodel ) {
@@ -469,8 +486,10 @@ class ApiQueryRevisions extends ApiQueryBase {
                if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
-                       } else {
-                               $comment = $revision->getComment();
+                               $anyHidden = true;
+                       }
+                       if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) {
+                               $comment = $revision->getRawComment();
 
                                if ( $this->fld_comment ) {
                                        $vals['comment'] = $comment;
@@ -507,7 +526,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                $content = null;
                global $wgParser;
                if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
-                       $content = $revision->getContent();
+                       $content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() );
                        // Expand templates after getting section content because
                        // template-added sections don't count and Parser::preprocess()
                        // will have less input
@@ -520,8 +539,14 @@ class ApiQueryRevisions extends ApiQueryBase {
                                        );
                                }
                        }
+                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $vals['texthidden'] = '';
+                               $anyHidden = true;
+                       } elseif ( !$content ) {
+                               $vals['textmissing'] = '';
+                       }
                }
-               if ( $this->fld_content && $content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
+               if ( $this->fld_content && $content ) {
                        $text = null;
 
                        if ( $this->generateXML ) {
@@ -596,21 +621,13 @@ class ApiQueryRevisions extends ApiQueryBase {
                        if ( $text !== false ) {
                                ApiResult::setContent( $vals, $text );
                        }
-               } elseif ( $this->fld_content ) {
-                       if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $vals['texthidden'] = '';
-                       } else {
-                               $vals['textmissing'] = '';
-                       }
                }
 
-               if ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) {
+               if ( $content && ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) ) {
                        global $wgAPIMaxUncachedDiffs;
                        static $n = 0; // Number of uncached diffs we've had
 
-                       if ( is_null( $content ) ) {
-                               $vals['textmissing'] = '';
-                       } elseif ( $n < $wgAPIMaxUncachedDiffs ) {
+                       if ( $n < $wgAPIMaxUncachedDiffs ) {
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
@@ -653,6 +670,10 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                }
 
+               if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = '';
+               }
+
                return $vals;
        }
 
@@ -660,6 +681,9 @@ class ApiQueryRevisions extends ApiQueryBase {
                if ( isset( $params['token'] ) ) {
                        return 'private';
                }
+               if ( $this->userCanSeeRevDel() ) {
+                       return 'private';
+               }
                if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) {
                        // formatComment() calls wfMessage() among other things
                        return 'anon-public-user-private';
index 1c9a93c..7896a2c 100644 (file)
@@ -176,9 +176,19 @@ class ApiQueryContributions extends ApiQueryBase {
                        );
                }
 
-               if ( !$user->isAllowed( 'hideuser' ) ) {
-                       $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' );
+               // Don't include any revisions where we're not supposed to be able to
+               // see the username.
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = Revision::DELETED_USER;
+               } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                       $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+               } else {
+                       $bitmask = 0;
+               }
+               if ( $bitmask ) {
+                       $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" );
                }
+
                // We only want pages by the specified users.
                if ( $this->prefixMode ) {
                        $this->addWhere( 'rev_user_text' .
@@ -299,11 +309,19 @@ class ApiQueryContributions extends ApiQueryBase {
         */
        private function extractRowInfo( $row ) {
                $vals = array();
+               $anyHidden = false;
+
+               if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
+                       $vals['texthidden'] = '';
+                       $anyHidden = true;
+               }
 
+               // Any rows where we can't view the user were filtered out in the query.
                $vals['userid'] = $row->rev_user;
                $vals['user'] = $row->rev_user_text;
                if ( $row->rev_deleted & Revision::DELETED_USER ) {
                        $vals['userhidden'] = '';
+                       $anyHidden = true;
                }
                if ( $this->fld_ids ) {
                        $vals['pageid'] = intval( $row->rev_page );
@@ -340,7 +358,9 @@ class ApiQueryContributions extends ApiQueryBase {
                if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) {
                        if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
                                $vals['commenthidden'] = '';
-                       } else {
+                               $anyHidden = true;
+                       }
+                       if ( Revision::userCanBitfield( $row->rev_deleted, Revision::DELETED_COMMENT, $this->getUser() ) ) {
                                if ( $this->fld_comment ) {
                                        $vals['comment'] = $row->rev_comment;
                                }
@@ -379,6 +399,10 @@ class ApiQueryContributions extends ApiQueryBase {
                        }
                }
 
+               if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) {
+                       $vals['suppressed'] = '';
+               }
+
                return $vals;
        }
 
index 3465e48..b7dc865 100644 (file)
@@ -59,7 +59,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
 
                $params = $this->extractRequestParams();
 
-               $user = $this->getWatchlistUser( $params );
+               $user = $this->getUser();
+               $wlowner = $this->getWatchlistUser( $params );
 
                if ( !is_null( $params['prop'] ) && is_null( $resultPageSet ) ) {
                        $prop = array_flip( $params['prop'] );
@@ -89,6 +90,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        'rc_title',
                        'rc_timestamp',
                        'rc_type',
+                       'rc_deleted',
                ) );
 
                if ( is_null( $resultPageSet ) ) {
@@ -120,7 +122,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        'watchlist',
                ) );
 
-               $userId = $user->getId();
+               $userId = $wlowner->getId();
                $this->addJoinConds( array( 'watchlist' => array( 'INNER JOIN',
                        array(
                                'wl_user' => $userId,
@@ -129,10 +131,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        )
                ) ) );
 
-               $this->addWhere( array(
-                       'rc_deleted' => 0,
-               ) );
-
                $db = $this->getDB();
 
                $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'],
@@ -159,7 +157,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
 
                        // Check permissions.
                        if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) {
-                               $user = $this->getUser();
                                if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
                                        $this->dieUsage(
                                                'You need the patrol right to request the patrolled flag',
@@ -199,6 +196,36 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        !isset( $params['start'] ) && !isset( $params['end'] ) && $db->getType() == 'mysql'
                );
 
+               // Paranoia: avoid brute force searches (bug 17342)
+               if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) {
+                       if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                               $bitmask = Revision::DELETED_USER;
+                       } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                               $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED;
+                       } else {
+                               $bitmask = 0;
+                       }
+                       if ( $bitmask ) {
+                               $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" );
+                       }
+               }
+
+               // LogPage::DELETED_ACTION hides the affected page, too. So hide those
+               // entirely from the watchlist, or someone could guess the title.
+               if ( !$user->isAllowed( 'deletedhistory' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION;
+               } elseif ( !$user->isAllowed( 'suppressrevision' ) ) {
+                       $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
+               } else {
+                       $bitmask = 0;
+               }
+               if ( $bitmask ) {
+                       $this->addWhere( $this->getDB()->makeList( array(
+                               'rc_type != ' . RC_LOG,
+                               $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
+                       ), LIST_OR ) );
+               }
+
                $this->addOption( 'LIMIT', $params['limit'] + 1 );
 
                $ids = array();
@@ -246,6 +273,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
        }
 
        private function extractRowInfo( $row ) {
+               /* Determine the title of the page that has been changed. */
+               $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
+               $user = $this->getUser();
+
+               /* Our output data. */
                $vals = array();
 
                $type = intval( $row->rc_type );
@@ -274,87 +306,131 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                                $vals['type'] = $type;
                }
 
-               if ( $this->fld_ids ) {
-                       $vals['pageid'] = intval( $row->rc_cur_id );
-                       $vals['revid'] = intval( $row->rc_this_oldid );
-                       $vals['old_revid'] = intval( $row->rc_last_oldid );
-               }
+               $anyHidden = false;
 
-               $title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
-
-               if ( $this->fld_title ) {
-                       ApiQueryBase::addTitleInfo( $vals, $title );
+               /* Create a new entry in the result for the title. */
+               if ( $this->fld_title || $this->fld_ids ) {
+                       // These should already have been filtered out of the query, but just in case.
+                       if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) {
+                               $vals['actionhidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( $type !== RC_LOG ||
+                               LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user )
+                       ) {
+                               if ( $this->fld_title ) {
+                                       ApiQueryBase::addTitleInfo( $vals, $title );
+                               }
+                               if ( $this->fld_ids ) {
+                                       $vals['pageid'] = intval( $row->rc_cur_id );
+                                       $vals['revid'] = intval( $row->rc_this_oldid );
+                                       $vals['old_revid'] = intval( $row->rc_last_oldid );
+                               }
+                       }
                }
 
+               /* Add user data and 'anon' flag, if user is anonymous. */
                if ( $this->fld_user || $this->fld_userid ) {
-
-                       if ( $this->fld_userid ) {
-                               $vals['userid'] = $row->rc_user;
-                               // for backwards compatibility
-                               $vals['user'] = $row->rc_user;
+                       if ( $row->rc_deleted & Revision::DELETED_USER ) {
+                               $vals['userhidden'] = '';
+                               $anyHidden = true;
                        }
+                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) {
+                               if ( $this->fld_userid ) {
+                                       $vals['userid'] = $row->rc_user;
+                                       // for backwards compatibility
+                                       $vals['user'] = $row->rc_user;
+                               }
 
-                       if ( $this->fld_user ) {
-                               $vals['user'] = $row->rc_user_text;
-                       }
+                               if ( $this->fld_user ) {
+                                       $vals['user'] = $row->rc_user_text;
+                               }
 
-                       if ( !$row->rc_user ) {
-                               $vals['anon'] = '';
+                               if ( !$row->rc_user ) {
+                                       $vals['anon'] = '';
+                               }
                        }
                }
 
+               /* Add flags, such as new, minor, bot. */
                if ( $this->fld_flags ) {
+                       if ( $row->rc_bot ) {
+                               $vals['bot'] = '';
+                       }
                        if ( $row->rc_type == RC_NEW ) {
                                $vals['new'] = '';
                        }
                        if ( $row->rc_minor ) {
                                $vals['minor'] = '';
                        }
-                       if ( $row->rc_bot ) {
-                               $vals['bot'] = '';
-                       }
                }
 
-               if ( $this->fld_patrol && isset( $row->rc_patrolled ) ) {
-                       $vals['patrolled'] = '';
+               /* Add sizes of each revision. (Only available on 1.10+) */
+               if ( $this->fld_sizes ) {
+                       $vals['oldlen'] = intval( $row->rc_old_len );
+                       $vals['newlen'] = intval( $row->rc_new_len );
                }
 
+               /* Add the timestamp. */
                if ( $this->fld_timestamp ) {
                        $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp );
                }
 
-               if ( $this->fld_sizes ) {
-                       $vals['oldlen'] = intval( $row->rc_old_len );
-                       $vals['newlen'] = intval( $row->rc_new_len );
-               }
-
                if ( $this->fld_notificationtimestamp ) {
                        $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null )
                                ? ''
                                : wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp );
                }
 
-               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
-                       $vals['comment'] = $row->rc_comment;
+               /* Add edit summary / log summary. */
+               if ( $this->fld_comment || $this->fld_parsedcomment ) {
+                       if ( $row->rc_deleted & Revision::DELETED_COMMENT ) {
+                               $vals['commenthidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) {
+                               if ( $this->fld_comment && isset( $row->rc_comment ) ) {
+                                       $vals['comment'] = $row->rc_comment;
+                               }
+
+                               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
+                                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+                               }
+                       }
+               }
+
+               /* Add the patrolled flag */
+               if ( $this->fld_patrol && $row->rc_patrolled == 1 ) {
+                       $vals['patrolled'] = '';
                }
 
-               if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) {
-                       $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title );
+               if ( $this->fld_patrol && ChangesList::isUnpatrolled( $row, $user ) ) {
+                       $vals['unpatrolled'] = '';
                }
 
                if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) {
-                       $vals['logid'] = intval( $row->rc_logid );
-                       $vals['logtype'] = $row->rc_log_type;
-                       $vals['logaction'] = $row->rc_log_action;
-                       $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
-                       ApiQueryLogEvents::addLogParams(
-                               $this->getResult(),
-                               $vals,
-                               $logEntry->getParameters(),
-                               $logEntry->getType(),
-                               $logEntry->getSubtype(),
-                               $logEntry->getTimestamp()
-                       );
+                       if ( $row->rc_deleted & LogPage::DELETED_ACTION ) {
+                               $vals['actionhidden'] = '';
+                               $anyHidden = true;
+                       }
+                       if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) {
+                               $vals['logid'] = intval( $row->rc_logid );
+                               $vals['logtype'] = $row->rc_log_type;
+                               $vals['logaction'] = $row->rc_log_action;
+                               $logEntry = DatabaseLogEntry::newFromRow( (array)$row );
+                               ApiQueryLogEvents::addLogParams(
+                                       $this->getResult(),
+                                       $vals,
+                                       $logEntry->getParameters(),
+                                       $logEntry->getType(),
+                                       $logEntry->getSubtype(),
+                                       $logEntry->getTimestamp()
+                               );
+                       }
+               }
+
+               if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) {
+                       $vals['suppressed'] = '';
                }
 
                return $vals;
@@ -467,7 +543,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        ),
                        'token' => array(
                                ApiBase::PARAM_TYPE => 'string'
-                       )
+                       ),
                );
        }
 
@@ -511,7 +587,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase {
                        ),
                        'owner' => 'The name of the user whose watchlist you\'d like to access',
                        'token' => 'Give a security token (settable in preferences) to ' .
-                               'allow access to another user\'s watchlist'
+                               'allow access to another user\'s watchlist',
                );
        }