* @ingroup SpecialPage
*/
use MediaWiki\Logger\LoggerFactory;
+use Wikimedia\Rdbms\DBQueryTimeoutError;
use Wikimedia\Rdbms\ResultWrapper;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
* @ingroup SpecialPage
*/
abstract class ChangesListSpecialPage extends SpecialPage {
+ /**
+ * Maximum length of a tag description in UTF-8 characters.
+ * Longer descriptions will be truncated.
+ */
+ const TAG_DESC_CHARACTER_LIMIT = 120;
+
/**
* Preference name for saved queries. Subclasses that use saved queries should override this.
* @var string
*/
protected static $savedQueriesPreferenceName;
+ /**
+ * Preference name for 'days'. Subclasses should override this.
+ * @var string
+ */
+ protected static $daysPreferenceName;
+
+ /**
+ * Preference name for 'limit'. Subclasses should override this.
+ * @var string
+ */
+ protected static $limitPreferenceName;
+
/** @var string */
protected $rcSubpage;
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_user = 0';
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
},
'isReplacedInStructuredUi' => true,
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_user != 0';
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
},
'isReplacedInStructuredUi' => true,
]
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $user = $ctx->getUser();
- $conds[] = 'rc_user_text != ' . $dbr->addQuotes( $user->getName() );
+ $actorQuery = ActorMigration::newMigration()->getWhere( $dbr, 'rc_user', $ctx->getUser() );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = 'NOT(' . $actorQuery['conds'] . ')';
},
'cssClassSuffix' => 'self',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $user = $ctx->getUser();
- $conds[] = 'rc_user_text = ' . $dbr->addQuotes( $user->getName() );
+ $actorQuery = ActorMigration::newMigration()
+ ->getWhere( $dbr, 'rc_user', $ctx->getUser(), false );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+ $conds[] = $actorQuery['conds'];
},
'cssClassSuffix' => 'others',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds,
&$query_options, &$join_conds
) {
- $conds[] = 'rc_patrolled = 1';
+ $conds[] = 'rc_patrolled != 0';
},
'cssClassSuffix' => 'unpatrolled',
'isRowApplicableCallable' => function ( $ctx, $rc ) {
public function execute( $subpage ) {
$this->rcSubpage = $subpage;
- $this->considerActionsForDefaultSavedQuery();
+ $this->considerActionsForDefaultSavedQuery( $subpage );
- $rows = $this->getRows();
$opts = $this->getOptions();
- if ( $rows === false ) {
- $rows = new FakeResultWrapper( [] );
- }
+ try {
+ $rows = $this->getRows();
+ if ( $rows === false ) {
+ $rows = new FakeResultWrapper( [] );
+ }
- // Used by Structured UI app to get results without MW chrome
- if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
- $this->getOutput()->setArticleBodyOnly( true );
- }
+ // Used by Structured UI app to get results without MW chrome
+ if ( $this->getRequest()->getVal( 'action' ) === 'render' ) {
+ $this->getOutput()->setArticleBodyOnly( true );
+ }
- // Used by "live update" and "view newest" to check
- // if there's new changes with minimal data transfer
- if ( $this->getRequest()->getBool( 'peek' ) ) {
- $code = $rows->numRows() > 0 ? 200 : 204;
- $this->getOutput()->setStatusCode( $code );
- return;
- }
+ // Used by "live update" and "view newest" to check
+ // if there's new changes with minimal data transfer
+ if ( $this->getRequest()->getBool( 'peek' ) ) {
+ $code = $rows->numRows() > 0 ? 200 : 204;
+ $this->getOutput()->setStatusCode( $code );
- $batch = new LinkBatch;
- foreach ( $rows as $row ) {
- $batch->add( NS_USER, $row->rc_user_text );
- $batch->add( NS_USER_TALK, $row->rc_user_text );
- $batch->add( $row->rc_namespace, $row->rc_title );
- if ( $row->rc_source === RecentChange::SRC_LOG ) {
- $formatter = LogFormatter::newFromRow( $row );
- foreach ( $formatter->getPreloadTitles() as $title ) {
- $batch->addObj( $title );
+ if ( $this->getUser()->isAnon() !==
+ $this->getRequest()->getFuzzyBool( 'isAnon' )
+ ) {
+ $this->getOutput()->setStatusCode( 205 );
}
+
+ return;
}
- }
- $batch->execute();
- $this->setHeaders();
- $this->outputHeader();
- $this->addModules();
- $this->webOutput( $rows, $opts );
+ $batch = new LinkBatch;
+ foreach ( $rows as $row ) {
+ $batch->add( NS_USER, $row->rc_user_text );
+ $batch->add( NS_USER_TALK, $row->rc_user_text );
+ $batch->add( $row->rc_namespace, $row->rc_title );
+ if ( $row->rc_source === RecentChange::SRC_LOG ) {
+ $formatter = LogFormatter::newFromRow( $row );
+ foreach ( $formatter->getPreloadTitles() as $title ) {
+ $batch->addObj( $title );
+ }
+ }
+ }
+ $batch->execute();
+
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+ $this->webOutput( $rows, $opts );
+
+ $rows->free();
+ } catch ( DBQueryTimeoutError $timeoutException ) {
+ MWExceptionHandler::logException( $timeoutException );
- $rows->free();
+ $this->setHeaders();
+ $this->outputHeader();
+ $this->addModules();
+
+ $this->getOutput()->setStatusCode( 500 );
+ $this->webOutputHeader( 0, $opts );
+ $this->outputTimeout();
+ }
if ( $this->getConfig()->get( 'EnableWANCacheReaper' ) ) {
// Clean up any bad page entries for titles showing up in RC
* Check whether or not the page should load defaults, and if so, whether
* a default saved query is relevant to be redirected to. If it is relevant,
* redirect properly with all necessary query parameters.
+ *
+ * @param string $subpage
*/
- protected function considerActionsForDefaultSavedQuery() {
- if ( !$this->isStructuredFilterUiEnabled() ) {
+ protected function considerActionsForDefaultSavedQuery( $subpage ) {
+ if ( !$this->isStructuredFilterUiEnabled() || $this->including() ) {
return;
}
// but are still valid and requested in the URL
$query = array_merge( $this->getRequest()->getValues(), $query );
unset( $query[ 'title' ] );
- $this->getOutput()->redirect( $this->getPageTitle()->getCanonicalURL( $query ) );
+ $this->getOutput()->redirect( $this->getPageTitle( $subpage )->getCanonicalURL( $query ) );
} else {
// There's a default, but the version is not 2, and the server can't
// actually recognize the query itself. This happens if it is before
*/
protected function includeRcFiltersApp() {
$out = $this->getOutput();
- if ( $this->isStructuredFilterUiEnabled() ) {
+ if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
$jsData = $this->getStructuredFilterJsData();
$messages = [];
'wgStructuredChangeFiltersSavedQueriesPreferenceName',
static::$savedQueriesPreferenceName
);
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersLimitPreferenceName',
+ static::$limitPreferenceName
+ );
+ $out->addJsConfigVars(
+ 'wgStructuredChangeFiltersDaysPreferenceName',
+ static::$daysPreferenceName
+ );
$out->addJsConfigVars(
'StructuredChangeFiltersLiveUpdatePollingRate',
isset( $explicitlyDefinedTags[ $tagName ] ) ||
isset( $softwareActivatedTags[ $tagName ] )
) {
- // Parse description
- $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
-
$result[] = [
'name' => $tagName,
'label' => Sanitizer::stripAllTags(
ChangeTags::tagDescription( $tagName, $context )
),
- 'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
+ 'description' =>
+ ChangeTags::truncateTagDescription(
+ $tagName, self::TAG_DESC_CHARACTER_LIMIT, $context
+ ),
'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
'hits' => $hits,
];
);
}
+ /**
+ * Add the "timeout" message to the output
+ */
+ protected function outputTimeout() {
+ $this->getOutput()->addHTML(
+ '<div class="mw-changeslist-empty mw-changeslist-timeout">' .
+ $this->msg( 'recentchanges-timeout' )->parse() .
+ '</div>'
+ );
+ }
+
/**
* Get the database result for this special page instance. Used by ApiFeedRecentChanges.
*
protected function doMainQuery( $tables, $fields, $conds,
$query_options, $join_conds, FormOptions $opts
) {
- $tables[] = 'recentchanges';
- $fields = array_merge( RecentChange::selectFields(), $fields );
+ $rcQuery = RecentChange::getQueryInfo();
+ $tables = array_merge( $tables, $rcQuery['tables'] );
+ $fields = array_merge( $rcQuery['fields'], $fields );
+ $join_conds = array_merge( $join_conds, $rcQuery['joins'] );
ChangeTags::modifyDisplayQuery(
$tables,
}
/**
- * Send output to the OutputPage object, only called if not used feeds
+ * Send header output to the OutputPage object, only called if not using feeds
*
- * @param ResultWrapper $rows Database rows
+ * @param int $rowCount Number of database rows
* @param FormOptions $opts
*/
- public function webOutput( $rows, $opts ) {
+ private function webOutputHeader( $rowCount, $opts ) {
if ( !$this->including() ) {
$this->outputFeedLinks();
- $this->doHeader( $opts, $rows->numRows() );
+ $this->doHeader( $opts, $rowCount );
}
+ }
+
+ /**
+ * Send output to the OutputPage object, only called if not used feeds
+ *
+ * @param ResultWrapper $rows Database rows
+ * @param FormOptions $opts
+ */
+ public function webOutput( $rows, $opts ) {
+ $this->webOutputHeader( $rows->numRows(), $opts );
$this->outputChangesList( $rows, $opts );
}
# Collapsible
$collapsedState = $this->getRequest()->getCookie( 'changeslist-state' );
$collapsedClass = $collapsedState === 'collapsed' ? ' mw-collapsed' : '';
+
$legend =
'<div class="mw-changeslist-legend mw-collapsible' . $collapsedClass . '">' .
$legendHeading .
] );
$out->addModules( 'mediawiki.special.changeslist.legend.js' );
- if ( $this->isStructuredFilterUiEnabled() ) {
+ if ( $this->isStructuredFilterUiEnabled() && !$this->including() ) {
$out->addModules( 'mediawiki.rcfilters.filters.ui' );
$out->addModuleStyles( 'mediawiki.rcfilters.filters.base.styles' );
}
return;
}
+ $actorMigration = ActorMigration::newMigration();
+ $actorQuery = $actorMigration->getJoin( 'rc_user' );
+ $tables += $actorQuery['tables'];
+ $join_conds += $actorQuery['joins'];
+
// 'registered' but not 'unregistered', experience levels, if any, are included in 'registered'
if (
in_array( 'registered', $selectedExpLevels ) &&
!in_array( 'unregistered', $selectedExpLevels )
) {
- $conds[] = 'rc_user != 0';
+ $conds[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
return;
}
if ( $selectedExpLevels === [ 'unregistered' ] ) {
- $conds[] = 'rc_user = 0';
+ $conds[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
return;
}
$tables[] = 'user';
- $join_conds['user'] = [ 'LEFT JOIN', 'rc_user = user_id' ];
+ $join_conds['user'] = [ 'LEFT JOIN', $actorQuery['fields']['rc_user'] . ' = user_id' ];
if ( $now === 0 ) {
$now = time();
if ( in_array( 'unregistered', $selectedExpLevels ) ) {
$selectedExpLevels = array_diff( $selectedExpLevels, [ 'unregistered' ] );
- $conditions[] = 'rc_user = 0';
+ $conditions[] = $actorMigration->isAnon( $actorQuery['fields']['rc_user'] );
}
if ( $selectedExpLevels === [ 'newcomer' ] ) {
} elseif ( $selectedExpLevels === [ 'experienced', 'learner' ] ) {
$conditions[] = $aboveNewcomer;
} elseif ( $selectedExpLevels === [ 'experienced', 'learner', 'newcomer' ] ) {
- $conditions[] = 'rc_user != 0';
+ $conditions[] = $actorMigration->isNotAnon( $actorQuery['fields']['rc_user'] );
}
if ( count( $conditions ) > 1 ) {
return true;
}
- if ( $this->getConfig()->get( 'StructuredChangeFiltersShowPreference' ) ) {
- return !$this->getUser()->getOption( 'rcenhancedfilters-disable' );
- } else {
- return $this->getUser()->getOption( 'rcenhancedfilters' );
- }
+ return static::checkStructuredFilterUiEnabled(
+ $this->getConfig(),
+ $this->getUser()
+ );
}
/**
}
}
- abstract function getDefaultLimit();
+ /**
+ * Static method to check whether StructuredFilter UI is enabled for the given user
+ *
+ * @since 1.31
+ * @param Config $config
+ * @param User $user
+ * @return bool
+ */
+ public static function checkStructuredFilterUiEnabled( Config $config, User $user ) {
+ if ( $config->get( 'StructuredChangeFiltersShowPreference' ) ) {
+ return !$user->getOption( 'rcenhancedfilters-disable' );
+ } else {
+ return $user->getOption( 'rcenhancedfilters' );
+ }
+ }
+
+ /**
+ * Get the default value of the number of changes to display when loading
+ * the result set.
+ *
+ * @since 1.30
+ * @return int
+ */
+ public function getDefaultLimit() {
+ return $this->getUser()->getIntOption( static::$limitPreferenceName );
+ }
/**
* Get the default value of the number of days to display when loading
* the result set.
* Supports fractional values, and should be cast to a float.
*
+ * @since 1.30
* @return float
*/
- abstract function getDefaultDays();
+ public function getDefaultDays() {
+ return floatval( $this->getUser()->getOption( static::$daysPreferenceName ) );
+ }
}