123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745 |
- <?php
- use MediaWiki\Linker\LinkTarget;
- use MediaWiki\Permissions\PermissionManager;
- use MediaWiki\Revision\RevisionRecord;
- use MediaWiki\User\UserIdentity;
- use Wikimedia\Assert\Assert;
- use Wikimedia\Rdbms\IDatabase;
- use Wikimedia\Rdbms\ILoadBalancer;
- /**
- * Class performing complex database queries related to WatchedItems.
- *
- * @since 1.28
- *
- * @file
- * @ingroup Watchlist
- *
- * @license GPL-2.0-or-later
- */
- class WatchedItemQueryService {
- const DIR_OLDER = 'older';
- const DIR_NEWER = 'newer';
- const INCLUDE_FLAGS = 'flags';
- const INCLUDE_USER = 'user';
- const INCLUDE_USER_ID = 'userid';
- const INCLUDE_COMMENT = 'comment';
- const INCLUDE_PATROL_INFO = 'patrol';
- const INCLUDE_AUTOPATROL_INFO = 'autopatrol';
- const INCLUDE_SIZES = 'sizes';
- const INCLUDE_LOG_INFO = 'loginfo';
- const INCLUDE_TAGS = 'tags';
- // FILTER_* constants are part of public API (are used in ApiQueryWatchlist and
- // ApiQueryWatchlistRaw classes) and should not be changed.
- // Changing values of those constants will result in a breaking change in the API
- const FILTER_MINOR = 'minor';
- const FILTER_NOT_MINOR = '!minor';
- const FILTER_BOT = 'bot';
- const FILTER_NOT_BOT = '!bot';
- const FILTER_ANON = 'anon';
- const FILTER_NOT_ANON = '!anon';
- const FILTER_PATROLLED = 'patrolled';
- const FILTER_NOT_PATROLLED = '!patrolled';
- const FILTER_AUTOPATROLLED = 'autopatrolled';
- const FILTER_NOT_AUTOPATROLLED = '!autopatrolled';
- const FILTER_UNREAD = 'unread';
- const FILTER_NOT_UNREAD = '!unread';
- const FILTER_CHANGED = 'changed';
- const FILTER_NOT_CHANGED = '!changed';
- const SORT_ASC = 'ASC';
- const SORT_DESC = 'DESC';
- /**
- * @var ILoadBalancer
- */
- private $loadBalancer;
- /** @var WatchedItemQueryServiceExtension[]|null */
- private $extensions = null;
- /** @var CommentStore */
- private $commentStore;
- /** @var ActorMigration */
- private $actorMigration;
- /** @var WatchedItemStoreInterface */
- private $watchedItemStore;
- /** @var PermissionManager */
- private $permissionManager;
- public function __construct(
- ILoadBalancer $loadBalancer,
- CommentStore $commentStore,
- ActorMigration $actorMigration,
- WatchedItemStoreInterface $watchedItemStore,
- PermissionManager $permissionManager
- ) {
- $this->loadBalancer = $loadBalancer;
- $this->commentStore = $commentStore;
- $this->actorMigration = $actorMigration;
- $this->watchedItemStore = $watchedItemStore;
- $this->permissionManager = $permissionManager;
- }
- /**
- * @return WatchedItemQueryServiceExtension[]
- */
- private function getExtensions() {
- if ( $this->extensions === null ) {
- $this->extensions = [];
- Hooks::run( 'WatchedItemQueryServiceExtensions', [ &$this->extensions, $this ] );
- }
- return $this->extensions;
- }
- /**
- * @return IDatabase
- */
- private function getConnection() {
- return $this->loadBalancer->getConnectionRef( DB_REPLICA, [ 'watchlist' ] );
- }
- /**
- * @param User $user
- * @param array $options Allowed keys:
- * 'includeFields' => string[] RecentChange fields to be included in the result,
- * self::INCLUDE_* constants should be used
- * 'filters' => string[] optional filters to narrow down resulted items
- * 'namespaceIds' => int[] optional namespace IDs to filter by
- * (defaults to all namespaces)
- * 'allRevisions' => bool return multiple revisions of the same page if true,
- * only the most recent if false (default)
- * 'rcTypes' => int[] which types of RecentChanges to include
- * (defaults to all types), allowed values: RC_EDIT, RC_NEW,
- * RC_LOG, RC_EXTERNAL, RC_CATEGORIZE
- * 'onlyByUser' => string only list changes by a specified user
- * 'notByUser' => string do not incluide changes by a specified user
- * 'dir' => string in which direction to enumerate, accepted values:
- * - DIR_OLDER list newest first
- * - DIR_NEWER list oldest first
- * 'start' => string (format accepted by wfTimestamp) requires 'dir' option,
- * timestamp to start enumerating from
- * 'end' => string (format accepted by wfTimestamp) requires 'dir' option,
- * timestamp to end enumerating
- * 'watchlistOwner' => User user whose watchlist items should be listed if different
- * than the one specified with $user param, requires
- * 'watchlistOwnerToken' option
- * 'watchlistOwnerToken' => string a watchlist token used to access another user's
- * watchlist, used with 'watchlistOwnerToken' option
- * 'limit' => int maximum numbers of items to return
- * 'usedInGenerator' => bool include only RecentChange id field required by the
- * generator ('rc_cur_id' or 'rc_this_oldid') if true, or all
- * id fields ('rc_cur_id', 'rc_this_oldid', 'rc_last_oldid')
- * if false (default)
- * @param array|null &$startFrom Continuation value: [ string $rcTimestamp, int $rcId ]
- * @return array[] Array of pairs ( WatchedItem $watchedItem, string[] $recentChangeInfo ),
- * where $recentChangeInfo contains the following keys:
- * - 'rc_id',
- * - 'rc_namespace',
- * - 'rc_title',
- * - 'rc_timestamp',
- * - 'rc_type',
- * - 'rc_deleted',
- * Additional keys could be added by specifying the 'includeFields' option
- */
- public function getWatchedItemsWithRecentChangeInfo(
- User $user, array $options = [], &$startFrom = null
- ) {
- $options += [
- 'includeFields' => [],
- 'namespaceIds' => [],
- 'filters' => [],
- 'allRevisions' => false,
- 'usedInGenerator' => false
- ];
- Assert::parameter(
- !isset( $options['rcTypes'] )
- || !array_diff( $options['rcTypes'], [ RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL, RC_CATEGORIZE ] ),
- '$options[\'rcTypes\']',
- 'must be an array containing only: RC_EDIT, RC_NEW, RC_LOG, RC_EXTERNAL and/or RC_CATEGORIZE'
- );
- Assert::parameter(
- !isset( $options['dir'] ) || in_array( $options['dir'], [ self::DIR_OLDER, self::DIR_NEWER ] ),
- '$options[\'dir\']',
- 'must be DIR_OLDER or DIR_NEWER'
- );
- Assert::parameter(
- !isset( $options['start'] ) && !isset( $options['end'] ) && $startFrom === null
- || isset( $options['dir'] ),
- '$options[\'dir\']',
- 'must be provided when providing the "start" or "end" options or the $startFrom parameter'
- );
- Assert::parameter(
- !isset( $options['startFrom'] ),
- '$options[\'startFrom\']',
- 'must not be provided, use $startFrom instead'
- );
- Assert::parameter(
- !isset( $startFrom ) || ( is_array( $startFrom ) && count( $startFrom ) === 2 ),
- '$startFrom',
- 'must be a two-element array'
- );
- if ( array_key_exists( 'watchlistOwner', $options ) ) {
- Assert::parameterType(
- User::class,
- $options['watchlistOwner'],
- '$options[\'watchlistOwner\']'
- );
- Assert::parameter(
- isset( $options['watchlistOwnerToken'] ),
- '$options[\'watchlistOwnerToken\']',
- 'must be provided when providing watchlistOwner option'
- );
- }
- $db = $this->getConnection();
- $tables = $this->getWatchedItemsWithRCInfoQueryTables( $options );
- $fields = $this->getWatchedItemsWithRCInfoQueryFields( $options );
- $conds = $this->getWatchedItemsWithRCInfoQueryConds( $db, $user, $options );
- $dbOptions = $this->getWatchedItemsWithRCInfoQueryDbOptions( $options );
- $joinConds = $this->getWatchedItemsWithRCInfoQueryJoinConds( $options );
- if ( $startFrom !== null ) {
- $conds[] = $this->getStartFromConds( $db, $options, $startFrom );
- }
- foreach ( $this->getExtensions() as $extension ) {
- $extension->modifyWatchedItemsWithRCInfoQuery(
- $user, $options, $db,
- $tables,
- $fields,
- $conds,
- $dbOptions,
- $joinConds
- );
- }
- $res = $db->select(
- $tables,
- $fields,
- $conds,
- __METHOD__,
- $dbOptions,
- $joinConds
- );
- $limit = $dbOptions['LIMIT'] ?? INF;
- $items = [];
- $startFrom = null;
- foreach ( $res as $row ) {
- if ( --$limit <= 0 ) {
- $startFrom = [ $row->rc_timestamp, $row->rc_id ];
- break;
- }
- $target = new TitleValue( (int)$row->rc_namespace, $row->rc_title );
- $items[] = [
- new WatchedItem(
- $user,
- $target,
- $this->watchedItemStore->getLatestNotificationTimestamp(
- $row->wl_notificationtimestamp, $user, $target
- )
- ),
- $this->getRecentChangeFieldsFromRow( $row )
- ];
- }
- foreach ( $this->getExtensions() as $extension ) {
- $extension->modifyWatchedItemsWithRCInfo( $user, $options, $db, $items, $res, $startFrom );
- }
- return $items;
- }
- /**
- * For simple listing of user's watchlist items, see WatchedItemStore::getWatchedItemsForUser
- *
- * @param UserIdentity $user
- * @param array $options Allowed keys:
- * 'sort' => string optional sorting by namespace ID and title
- * one of the self::SORT_* constants
- * 'namespaceIds' => int[] optional namespace IDs to filter by (defaults to all namespaces)
- * 'limit' => int maximum number of items to return
- * 'filter' => string optional filter, one of the self::FILTER_* contants
- * 'from' => LinkTarget requires 'sort' key, only return items starting from
- * those related to the link target
- * 'until' => LinkTarget requires 'sort' key, only return items until
- * those related to the link target
- * 'startFrom' => LinkTarget requires 'sort' key, only return items starting from
- * those related to the link target, allows to skip some link targets
- * specified using the form option
- * @return WatchedItem[]
- */
- public function getWatchedItemsForUser( UserIdentity $user, array $options = [] ) {
- if ( !$user->isRegistered() ) {
- // TODO: should this just return an empty array or rather complain loud at this point
- // as e.g. ApiBase::getWatchlistUser does?
- return [];
- }
- $options += [ 'namespaceIds' => [] ];
- Assert::parameter(
- !isset( $options['sort'] ) || in_array( $options['sort'], [ self::SORT_ASC, self::SORT_DESC ] ),
- '$options[\'sort\']',
- 'must be SORT_ASC or SORT_DESC'
- );
- Assert::parameter(
- !isset( $options['filter'] ) || in_array(
- $options['filter'], [ self::FILTER_CHANGED, self::FILTER_NOT_CHANGED ]
- ),
- '$options[\'filter\']',
- 'must be FILTER_CHANGED or FILTER_NOT_CHANGED'
- );
- Assert::parameter(
- !isset( $options['from'] ) && !isset( $options['until'] ) && !isset( $options['startFrom'] )
- || isset( $options['sort'] ),
- '$options[\'sort\']',
- 'must be provided if any of "from", "until", "startFrom" options is provided'
- );
- $db = $this->getConnection();
- $conds = $this->getWatchedItemsForUserQueryConds( $db, $user, $options );
- $dbOptions = $this->getWatchedItemsForUserQueryDbOptions( $options );
- $res = $db->select(
- 'watchlist',
- [ 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ],
- $conds,
- __METHOD__,
- $dbOptions
- );
- $watchedItems = [];
- foreach ( $res as $row ) {
- $target = new TitleValue( (int)$row->wl_namespace, $row->wl_title );
- // todo these could all be cached at some point?
- $watchedItems[] = new WatchedItem(
- $user,
- $target,
- $this->watchedItemStore->getLatestNotificationTimestamp(
- $row->wl_notificationtimestamp, $user, $target
- )
- );
- }
- return $watchedItems;
- }
- private function getRecentChangeFieldsFromRow( stdClass $row ) {
- // FIXME: This can be simplified to single array_filter call filtering by key value,
- // now we have stopped supporting PHP 5.5
- $allFields = get_object_vars( $row );
- $rcKeys = array_filter(
- array_keys( $allFields ),
- function ( $key ) {
- return substr( $key, 0, 3 ) === 'rc_';
- }
- );
- return array_intersect_key( $allFields, array_flip( $rcKeys ) );
- }
- private function getWatchedItemsWithRCInfoQueryTables( array $options ) {
- $tables = [ 'recentchanges', 'watchlist' ];
- if ( !$options['allRevisions'] ) {
- $tables[] = 'page';
- }
- if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $tables += $this->commentStore->getJoin( 'rc_comment' )['tables'];
- }
- if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
- in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
- in_array( self::FILTER_ANON, $options['filters'] ) ||
- in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
- array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
- ) {
- $tables += $this->actorMigration->getJoin( 'rc_user' )['tables'];
- }
- return $tables;
- }
- private function getWatchedItemsWithRCInfoQueryFields( array $options ) {
- $fields = [
- 'rc_id',
- 'rc_namespace',
- 'rc_title',
- 'rc_timestamp',
- 'rc_type',
- 'rc_deleted',
- 'wl_notificationtimestamp'
- ];
- $rcIdFields = [
- 'rc_cur_id',
- 'rc_this_oldid',
- 'rc_last_oldid',
- ];
- if ( $options['usedInGenerator'] ) {
- if ( $options['allRevisions'] ) {
- $rcIdFields = [ 'rc_this_oldid' ];
- } else {
- $rcIdFields = [ 'rc_cur_id' ];
- }
- }
- $fields = array_merge( $fields, $rcIdFields );
- if ( in_array( self::INCLUDE_FLAGS, $options['includeFields'] ) ) {
- $fields = array_merge( $fields, [ 'rc_type', 'rc_minor', 'rc_bot' ] );
- }
- if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ) {
- $fields['rc_user_text'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user_text'];
- }
- if ( in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ) {
- $fields['rc_user'] = $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user'];
- }
- if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $fields += $this->commentStore->getJoin( 'rc_comment' )['fields'];
- }
- if ( in_array( self::INCLUDE_PATROL_INFO, $options['includeFields'] ) ) {
- $fields = array_merge( $fields, [ 'rc_patrolled', 'rc_log_type' ] );
- }
- if ( in_array( self::INCLUDE_SIZES, $options['includeFields'] ) ) {
- $fields = array_merge( $fields, [ 'rc_old_len', 'rc_new_len' ] );
- }
- if ( in_array( self::INCLUDE_LOG_INFO, $options['includeFields'] ) ) {
- $fields = array_merge( $fields, [ 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ] );
- }
- if ( in_array( self::INCLUDE_TAGS, $options['includeFields'] ) ) {
- // prefixed with rc_ to include the field in getRecentChangeFieldsFromRow
- $fields['rc_tags'] = ChangeTags::makeTagSummarySubquery( 'recentchanges' );
- }
- return $fields;
- }
- private function getWatchedItemsWithRCInfoQueryConds(
- IDatabase $db,
- User $user,
- array $options
- ) {
- $watchlistOwnerId = $this->getWatchlistOwnerId( $user, $options );
- $conds = [ 'wl_user' => $watchlistOwnerId ];
- if ( !$options['allRevisions'] ) {
- $conds[] = $db->makeList(
- [ 'rc_this_oldid=page_latest', 'rc_type=' . RC_LOG ],
- LIST_OR
- );
- }
- if ( $options['namespaceIds'] ) {
- $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
- }
- if ( array_key_exists( 'rcTypes', $options ) ) {
- $conds['rc_type'] = array_map( 'intval', $options['rcTypes'] );
- }
- $conds = array_merge(
- $conds,
- $this->getWatchedItemsWithRCInfoQueryFilterConds( $user, $options )
- );
- $conds = array_merge( $conds, $this->getStartEndConds( $db, $options ) );
- if ( !isset( $options['start'] ) && !isset( $options['end'] ) && $db->getType() === 'mysql' ) {
- // This is an index optimization for mysql
- $conds[] = 'rc_timestamp > ' . $db->addQuotes( '' );
- }
- $conds = array_merge( $conds, $this->getUserRelatedConds( $db, $user, $options ) );
- $deletedPageLogCond = $this->getExtraDeletedPageLogEntryRelatedCond( $db, $user );
- if ( $deletedPageLogCond ) {
- $conds[] = $deletedPageLogCond;
- }
- return $conds;
- }
- private function getWatchlistOwnerId( UserIdentity $user, array $options ) {
- if ( array_key_exists( 'watchlistOwner', $options ) ) {
- /** @var User $watchlistOwner */
- $watchlistOwner = $options['watchlistOwner'];
- $ownersToken =
- $watchlistOwner->getOption( 'watchlisttoken' );
- $token = $options['watchlistOwnerToken'];
- if ( $ownersToken == '' || !hash_equals( $ownersToken, $token ) ) {
- throw ApiUsageException::newWithMessage( null, 'apierror-bad-watchlist-token', 'bad_wltoken' );
- }
- return $watchlistOwner->getId();
- }
- return $user->getId();
- }
- private function getWatchedItemsWithRCInfoQueryFilterConds( User $user, array $options ) {
- $conds = [];
- if ( in_array( self::FILTER_MINOR, $options['filters'] ) ) {
- $conds[] = 'rc_minor != 0';
- } elseif ( in_array( self::FILTER_NOT_MINOR, $options['filters'] ) ) {
- $conds[] = 'rc_minor = 0';
- }
- if ( in_array( self::FILTER_BOT, $options['filters'] ) ) {
- $conds[] = 'rc_bot != 0';
- } elseif ( in_array( self::FILTER_NOT_BOT, $options['filters'] ) ) {
- $conds[] = 'rc_bot = 0';
- }
- if ( in_array( self::FILTER_ANON, $options['filters'] ) ) {
- $conds[] = $this->actorMigration->isAnon(
- $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
- );
- } elseif ( in_array( self::FILTER_NOT_ANON, $options['filters'] ) ) {
- $conds[] = $this->actorMigration->isNotAnon(
- $this->actorMigration->getJoin( 'rc_user' )['fields']['rc_user']
- );
- }
- if ( $user->useRCPatrol() || $user->useNPPatrol() ) {
- // TODO: not sure if this should simply ignore patrolled filters if user does not have the patrol
- // right, or maybe rather fail loud at this point, same as e.g. ApiQueryWatchlist does?
- if ( in_array( self::FILTER_PATROLLED, $options['filters'] ) ) {
- $conds[] = 'rc_patrolled != ' . RecentChange::PRC_UNPATROLLED;
- } elseif ( in_array( self::FILTER_NOT_PATROLLED, $options['filters'] ) ) {
- $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
- }
- if ( in_array( self::FILTER_AUTOPATROLLED, $options['filters'] ) ) {
- $conds['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
- } elseif ( in_array( self::FILTER_NOT_AUTOPATROLLED, $options['filters'] ) ) {
- $conds[] = 'rc_patrolled != ' . RecentChange::PRC_AUTOPATROLLED;
- }
- }
- if ( in_array( self::FILTER_UNREAD, $options['filters'] ) ) {
- $conds[] = 'rc_timestamp >= wl_notificationtimestamp';
- } elseif ( in_array( self::FILTER_NOT_UNREAD, $options['filters'] ) ) {
- // TODO: should this be changed to use Database::makeList?
- $conds[] = 'wl_notificationtimestamp IS NULL OR rc_timestamp < wl_notificationtimestamp';
- }
- return $conds;
- }
- private function getStartEndConds( IDatabase $db, array $options ) {
- if ( !isset( $options['start'] ) && !isset( $options['end'] ) ) {
- return [];
- }
- $conds = [];
- if ( isset( $options['start'] ) ) {
- $after = $options['dir'] === self::DIR_OLDER ? '<=' : '>=';
- $conds[] = 'rc_timestamp ' . $after . ' ' .
- $db->addQuotes( $db->timestamp( $options['start'] ) );
- }
- if ( isset( $options['end'] ) ) {
- $before = $options['dir'] === self::DIR_OLDER ? '>=' : '<=';
- $conds[] = 'rc_timestamp ' . $before . ' ' .
- $db->addQuotes( $db->timestamp( $options['end'] ) );
- }
- return $conds;
- }
- private function getUserRelatedConds( IDatabase $db, UserIdentity $user, array $options ) {
- if ( !array_key_exists( 'onlyByUser', $options ) && !array_key_exists( 'notByUser', $options ) ) {
- return [];
- }
- $conds = [];
- if ( array_key_exists( 'onlyByUser', $options ) ) {
- $byUser = User::newFromName( $options['onlyByUser'], false );
- $conds[] = $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'];
- } elseif ( array_key_exists( 'notByUser', $options ) ) {
- $byUser = User::newFromName( $options['notByUser'], false );
- $conds[] = 'NOT(' . $this->actorMigration->getWhere( $db, 'rc_user', $byUser )['conds'] . ')';
- }
- // Avoid brute force searches (T19342)
- $bitmask = 0;
- if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
- $bitmask = RevisionRecord::DELETED_USER;
- } elseif ( !$this->permissionManager
- ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
- ) {
- $bitmask = RevisionRecord::DELETED_USER | RevisionRecord::DELETED_RESTRICTED;
- }
- if ( $bitmask ) {
- $conds[] = $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask";
- }
- return $conds;
- }
- private function getExtraDeletedPageLogEntryRelatedCond( IDatabase $db, UserIdentity $user ) {
- // LogPage::DELETED_ACTION hides the affected page, too. So hide those
- // entirely from the watchlist, or someone could guess the title.
- $bitmask = 0;
- if ( !$this->permissionManager->userHasRight( $user, 'deletedhistory' ) ) {
- $bitmask = LogPage::DELETED_ACTION;
- } elseif ( !$this->permissionManager
- ->userHasAnyRight( $user, 'suppressrevision', 'viewsuppressed' )
- ) {
- $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED;
- }
- if ( $bitmask ) {
- return $db->makeList( [
- 'rc_type != ' . RC_LOG,
- $db->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask",
- ], LIST_OR );
- }
- return '';
- }
- private function getStartFromConds( IDatabase $db, array $options, array $startFrom ) {
- $op = $options['dir'] === self::DIR_OLDER ? '<' : '>';
- list( $rcTimestamp, $rcId ) = $startFrom;
- $rcTimestamp = $db->addQuotes( $db->timestamp( $rcTimestamp ) );
- $rcId = (int)$rcId;
- return $db->makeList(
- [
- "rc_timestamp $op $rcTimestamp",
- $db->makeList(
- [
- "rc_timestamp = $rcTimestamp",
- "rc_id $op= $rcId"
- ],
- LIST_AND
- )
- ],
- LIST_OR
- );
- }
- private function getWatchedItemsForUserQueryConds(
- IDatabase $db, UserIdentity $user, array $options
- ) {
- $conds = [ 'wl_user' => $user->getId() ];
- if ( $options['namespaceIds'] ) {
- $conds['wl_namespace'] = array_map( 'intval', $options['namespaceIds'] );
- }
- if ( isset( $options['filter'] ) ) {
- $filter = $options['filter'];
- if ( $filter === self::FILTER_CHANGED ) {
- $conds[] = 'wl_notificationtimestamp IS NOT NULL';
- } else {
- $conds[] = 'wl_notificationtimestamp IS NULL';
- }
- }
- if ( isset( $options['from'] ) ) {
- $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
- $conds[] = $this->getFromUntilTargetConds( $db, $options['from'], $op );
- }
- if ( isset( $options['until'] ) ) {
- $op = $options['sort'] === self::SORT_ASC ? '<' : '>';
- $conds[] = $this->getFromUntilTargetConds( $db, $options['until'], $op );
- }
- if ( isset( $options['startFrom'] ) ) {
- $op = $options['sort'] === self::SORT_ASC ? '>' : '<';
- $conds[] = $this->getFromUntilTargetConds( $db, $options['startFrom'], $op );
- }
- return $conds;
- }
- /**
- * Creates a query condition part for getting only items before or after the given link target
- * (while ordering using $sort mode)
- *
- * @param IDatabase $db
- * @param LinkTarget $target
- * @param string $op comparison operator to use in the conditions
- * @return string
- */
- private function getFromUntilTargetConds( IDatabase $db, LinkTarget $target, $op ) {
- return $db->makeList(
- [
- "wl_namespace $op " . $target->getNamespace(),
- $db->makeList(
- [
- 'wl_namespace = ' . $target->getNamespace(),
- "wl_title $op= " . $db->addQuotes( $target->getDBkey() )
- ],
- LIST_AND
- )
- ],
- LIST_OR
- );
- }
- private function getWatchedItemsWithRCInfoQueryDbOptions( array $options ) {
- $dbOptions = [];
- if ( array_key_exists( 'dir', $options ) ) {
- $sort = $options['dir'] === self::DIR_OLDER ? ' DESC' : '';
- $dbOptions['ORDER BY'] = [ 'rc_timestamp' . $sort, 'rc_id' . $sort ];
- }
- if ( array_key_exists( 'limit', $options ) ) {
- $dbOptions['LIMIT'] = (int)$options['limit'] + 1;
- }
- return $dbOptions;
- }
- private function getWatchedItemsForUserQueryDbOptions( array $options ) {
- $dbOptions = [];
- if ( array_key_exists( 'sort', $options ) ) {
- $dbOptions['ORDER BY'] = [
- "wl_namespace {$options['sort']}",
- "wl_title {$options['sort']}"
- ];
- if ( count( $options['namespaceIds'] ) === 1 ) {
- $dbOptions['ORDER BY'] = "wl_title {$options['sort']}";
- }
- }
- if ( array_key_exists( 'limit', $options ) ) {
- $dbOptions['LIMIT'] = (int)$options['limit'];
- }
- return $dbOptions;
- }
- private function getWatchedItemsWithRCInfoQueryJoinConds( array $options ) {
- $joinConds = [
- 'watchlist' => [ 'JOIN',
- [
- 'wl_namespace=rc_namespace',
- 'wl_title=rc_title'
- ]
- ]
- ];
- if ( !$options['allRevisions'] ) {
- $joinConds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
- }
- if ( in_array( self::INCLUDE_COMMENT, $options['includeFields'] ) ) {
- $joinConds += $this->commentStore->getJoin( 'rc_comment' )['joins'];
- }
- if ( in_array( self::INCLUDE_USER, $options['includeFields'] ) ||
- in_array( self::INCLUDE_USER_ID, $options['includeFields'] ) ||
- in_array( self::FILTER_ANON, $options['filters'] ) ||
- in_array( self::FILTER_NOT_ANON, $options['filters'] ) ||
- array_key_exists( 'onlyByUser', $options ) || array_key_exists( 'notByUser', $options )
- ) {
- $joinConds += $this->actorMigration->getJoin( 'rc_user' )['joins'];
- }
- return $joinConds;
- }
- }
|