From: Brad Jorsch Date: Wed, 18 Dec 2013 21:58:39 +0000 (-0500) Subject: Improve API query RevDel handling X-Git-Tag: 1.31.0-rc.0~17221^2 X-Git-Url: http://git.cyclocoop.org/%24image?a=commitdiff_plain;h=48de797fbd19c60585a4934a9ba4b618a45ae65b;p=lhc%2Fweb%2Fwiklou.git Improve API query RevDel handling * 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 --- diff --git a/RELEASE-NOTES-1.23 b/RELEASE-NOTES-1.23 index 605e4ec803..a5e223b56a 100644 --- a/RELEASE-NOTES-1.23 +++ b/RELEASE-NOTES-1.23 @@ -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 === diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 32a03f13d0..eb510953c7 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -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' ); + } } /** diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 4e4b2cce23..7585ba760c 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -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; diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index e2c9964379..bcbc64245a 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -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; } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index bf11a76f19..cfc858a3a9 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -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'; } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index e4ce256504..1578775a1c 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -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'; diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index b298003fed..6ccc2886c4 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -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'; diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index ea992b3a07..bd5c51290d 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -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'; diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 1c9a93c216..7896a2cc25 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -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; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 3465e48c79..b7dc86543c 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -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', ); }