408 lines
12 KiB
PHP
408 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
* @ingroup Pager
|
|
*/
|
|
|
|
namespace MediaWiki\Pager;
|
|
|
|
use ChangesList;
|
|
use ChangeTags;
|
|
use MapCacheLRU;
|
|
use MediaWiki\Cache\LinkBatchFactory;
|
|
use MediaWiki\ChangeTags\ChangeTagsStore;
|
|
use MediaWiki\CommentFormatter\RowCommentFormatter;
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
|
use MediaWiki\Context\IContextSource;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\HookRunner;
|
|
use MediaWiki\Html\FormOptions;
|
|
use MediaWiki\Html\Html;
|
|
use MediaWiki\Linker\Linker;
|
|
use MediaWiki\Linker\LinkRenderer;
|
|
use MediaWiki\Parser\Sanitizer;
|
|
use MediaWiki\Permissions\GroupPermissionsLookup;
|
|
use MediaWiki\Revision\MutableRevisionRecord;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
use MediaWiki\Title\NamespaceInfo;
|
|
use MediaWiki\Title\Title;
|
|
use MediaWiki\User\TempUser\TempUserConfig;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
use RecentChange;
|
|
use stdClass;
|
|
use Wikimedia\Rdbms\IExpression;
|
|
|
|
/**
|
|
* @internal For use by SpecialNewPages
|
|
* @ingroup RecentChanges
|
|
* @ingroup Pager
|
|
*/
|
|
class NewPagesPager extends ReverseChronologicalPager {
|
|
|
|
/**
|
|
* @var FormOptions
|
|
*/
|
|
protected $opts;
|
|
|
|
protected MapCacheLRU $tagsCache;
|
|
|
|
/** @var string[] */
|
|
private $formattedComments = [];
|
|
/** @var bool Whether to group items by date by default this is disabled, but eventually the intention
|
|
* should be to default to true once all pages have been transitioned to support date grouping.
|
|
*/
|
|
public $mGroupByDate = true;
|
|
|
|
private GroupPermissionsLookup $groupPermissionsLookup;
|
|
private HookRunner $hookRunner;
|
|
private LinkBatchFactory $linkBatchFactory;
|
|
private NamespaceInfo $namespaceInfo;
|
|
private ChangeTagsStore $changeTagsStore;
|
|
private RowCommentFormatter $rowCommentFormatter;
|
|
private IContentHandlerFactory $contentHandlerFactory;
|
|
private TempUserConfig $tempUserConfig;
|
|
|
|
/**
|
|
* @param IContextSource $context
|
|
* @param LinkRenderer $linkRenderer
|
|
* @param GroupPermissionsLookup $groupPermissionsLookup
|
|
* @param HookContainer $hookContainer
|
|
* @param LinkBatchFactory $linkBatchFactory
|
|
* @param NamespaceInfo $namespaceInfo
|
|
* @param ChangeTagsStore $changeTagsStore
|
|
* @param RowCommentFormatter $rowCommentFormatter
|
|
* @param IContentHandlerFactory $contentHandlerFactory
|
|
* @param TempUserConfig $tempUserConfig
|
|
* @param FormOptions $opts
|
|
*/
|
|
public function __construct(
|
|
IContextSource $context,
|
|
LinkRenderer $linkRenderer,
|
|
GroupPermissionsLookup $groupPermissionsLookup,
|
|
HookContainer $hookContainer,
|
|
LinkBatchFactory $linkBatchFactory,
|
|
NamespaceInfo $namespaceInfo,
|
|
ChangeTagsStore $changeTagsStore,
|
|
RowCommentFormatter $rowCommentFormatter,
|
|
IContentHandlerFactory $contentHandlerFactory,
|
|
TempUserConfig $tempUserConfig,
|
|
FormOptions $opts
|
|
) {
|
|
parent::__construct( $context, $linkRenderer );
|
|
$this->groupPermissionsLookup = $groupPermissionsLookup;
|
|
$this->hookRunner = new HookRunner( $hookContainer );
|
|
$this->linkBatchFactory = $linkBatchFactory;
|
|
$this->namespaceInfo = $namespaceInfo;
|
|
$this->changeTagsStore = $changeTagsStore;
|
|
$this->rowCommentFormatter = $rowCommentFormatter;
|
|
$this->contentHandlerFactory = $contentHandlerFactory;
|
|
$this->tempUserConfig = $tempUserConfig;
|
|
$this->opts = $opts;
|
|
$this->tagsCache = new MapCacheLRU( 50 );
|
|
}
|
|
|
|
public function getQueryInfo() {
|
|
$rcQuery = RecentChange::getQueryInfo();
|
|
|
|
$conds = [];
|
|
$conds['rc_new'] = 1;
|
|
|
|
$username = $this->opts->getValue( 'username' );
|
|
$user = Title::makeTitleSafe( NS_USER, $username );
|
|
|
|
$size = abs( intval( $this->opts->getValue( 'size' ) ) );
|
|
if ( $size > 0 ) {
|
|
$db = $this->getDatabase();
|
|
if ( $this->opts->getValue( 'size-mode' ) === 'max' ) {
|
|
$conds[] = $db->expr( 'page_len', '<=', $size );
|
|
} else {
|
|
$conds[] = $db->expr( 'page_len', '>=', $size );
|
|
}
|
|
}
|
|
|
|
if ( $user ) {
|
|
$conds['actor_name'] = $user->getText();
|
|
} elseif ( $this->opts->getValue( 'hideliu' ) ) {
|
|
// Only include anonymous users if the 'hideliu' option has been provided.
|
|
$anonOnlyExpr = $this->getDatabase()->expr( 'actor_user', '=', null );
|
|
if ( $this->tempUserConfig->isKnown() ) {
|
|
$anonOnlyExpr = $anonOnlyExpr->orExpr( $this->tempUserConfig->getMatchCondition(
|
|
$this->getDatabase(), 'actor_name', IExpression::LIKE
|
|
) );
|
|
}
|
|
$conds[] = $anonOnlyExpr;
|
|
}
|
|
|
|
$conds = array_merge( $conds, $this->getNamespaceCond() );
|
|
|
|
# If this user cannot see patrolled edits or they are off, don't do dumb queries!
|
|
if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
|
|
$conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
|
|
}
|
|
|
|
if ( $this->opts->getValue( 'hidebots' ) ) {
|
|
$conds['rc_bot'] = 0;
|
|
}
|
|
|
|
if ( $this->opts->getValue( 'hideredirs' ) ) {
|
|
$conds['page_is_redirect'] = 0;
|
|
}
|
|
|
|
// Allow changes to the New Pages query
|
|
$tables = array_merge( $rcQuery['tables'], [ 'page' ] );
|
|
$fields = array_merge( $rcQuery['fields'], [
|
|
'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title',
|
|
'page_content_model',
|
|
] );
|
|
$join_conds = [ 'page' => [ 'JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
|
|
|
|
$this->hookRunner->onSpecialNewpagesConditions(
|
|
$this, $this->opts, $conds, $tables, $fields, $join_conds );
|
|
|
|
$info = [
|
|
'tables' => $tables,
|
|
'fields' => $fields,
|
|
'conds' => $conds,
|
|
'options' => [],
|
|
'join_conds' => $join_conds
|
|
];
|
|
|
|
// Modify query for tags
|
|
$this->changeTagsStore->modifyDisplayQuery(
|
|
$info['tables'],
|
|
$info['fields'],
|
|
$info['conds'],
|
|
$info['join_conds'],
|
|
$info['options'],
|
|
$this->opts['tagfilter'],
|
|
$this->opts['tagInvert']
|
|
);
|
|
|
|
return $info;
|
|
}
|
|
|
|
// Based on ContribsPager.php
|
|
private function getNamespaceCond() {
|
|
$namespace = $this->opts->getValue( 'namespace' );
|
|
if ( $namespace === 'all' || $namespace === '' ) {
|
|
return [];
|
|
}
|
|
|
|
$namespace = intval( $namespace );
|
|
if ( $namespace < NS_MAIN ) {
|
|
// Negative namespaces are invalid
|
|
return [];
|
|
}
|
|
|
|
$invert = $this->opts->getValue( 'invert' );
|
|
$associated = $this->opts->getValue( 'associated' );
|
|
|
|
$eq_op = $invert ? '!=' : '=';
|
|
$dbr = $this->getDatabase();
|
|
$namespaces = [ $namespace ];
|
|
if ( $associated ) {
|
|
$namespaces[] = $this->namespaceInfo->getAssociated( $namespace );
|
|
}
|
|
|
|
return [ $dbr->expr( 'rc_namespace', $eq_op, $namespaces ) ];
|
|
}
|
|
|
|
public function getIndexField() {
|
|
return [ [ 'rc_timestamp', 'rc_id' ] ];
|
|
}
|
|
|
|
public function formatRow( $row ) {
|
|
$title = Title::newFromRow( $row );
|
|
|
|
// Revision deletion works on revisions,
|
|
// so cast our recent change row to a revision row.
|
|
$revRecord = $this->revisionFromRcResult( $row, $title );
|
|
|
|
$classes = [];
|
|
$attribs = [ 'data-mw-revid' => $row->rc_this_oldid ];
|
|
|
|
$lang = $this->getLanguage();
|
|
$time = ChangesList::revDateLink( $revRecord, $this->getUser(), $lang, null, 'mw-newpages-time' );
|
|
|
|
$linkRenderer = $this->getLinkRenderer();
|
|
|
|
$query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
|
|
|
|
$plink = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ], $linkRenderer->makeKnownLink(
|
|
$title,
|
|
null,
|
|
[ 'class' => 'mw-newpages-pagename' ],
|
|
$query
|
|
) );
|
|
$linkArr = [];
|
|
$linkArr[] = $linkRenderer->makeKnownLink(
|
|
$title,
|
|
$this->msg( 'hist' )->text(),
|
|
[ 'class' => 'mw-newpages-history' ],
|
|
[ 'action' => 'history' ]
|
|
);
|
|
if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
|
|
->supportsDirectEditing()
|
|
) {
|
|
$linkArr[] = $linkRenderer->makeKnownLink(
|
|
$title,
|
|
$this->msg( 'editlink' )->text(),
|
|
[ 'class' => 'mw-newpages-edit' ],
|
|
[ 'action' => 'edit' ]
|
|
);
|
|
}
|
|
$links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
|
|
->pipeList( $linkArr ) )->escaped();
|
|
|
|
$length = Html::rawElement(
|
|
'span',
|
|
[ 'class' => 'mw-newpages-length' ],
|
|
$this->msg( 'brackets' )->rawParams(
|
|
$this->msg( 'nbytes' )->numParams( $row->length )->escaped()
|
|
)->escaped()
|
|
);
|
|
|
|
$ulink = Linker::revUserTools( $revRecord );
|
|
$rc = RecentChange::newFromRow( $row );
|
|
if ( ChangesList::userCan( $rc, RevisionRecord::DELETED_COMMENT, $this->getAuthority() ) ) {
|
|
$comment = $this->formattedComments[$rc->mAttribs['rc_id']];
|
|
} else {
|
|
$comment = '<span class="comment">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
|
|
}
|
|
if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
|
|
$deletedClass = 'history-deleted';
|
|
if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
|
|
$deletedClass .= ' mw-history-suppressed';
|
|
}
|
|
$comment = '<span class="' . $deletedClass . ' comment">' . $comment . '</span>';
|
|
}
|
|
|
|
if ( $this->getUser()->useNPPatrol() && !$row->rc_patrolled ) {
|
|
$classes[] = 'not-patrolled';
|
|
}
|
|
|
|
# Add a class for zero byte pages
|
|
if ( $row->length == 0 ) {
|
|
$classes[] = 'mw-newpages-zero-byte-page';
|
|
}
|
|
|
|
# Tags, if any.
|
|
if ( isset( $row->ts_tags ) ) {
|
|
[ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
|
|
$this->tagsCache->makeKey(
|
|
$row->ts_tags,
|
|
$this->getUser()->getName(),
|
|
$lang->getCode()
|
|
),
|
|
fn () => ChangeTags::formatSummaryRow(
|
|
$row->ts_tags,
|
|
'newpages',
|
|
$this->getContext()
|
|
)
|
|
);
|
|
$classes = array_merge( $classes, $newClasses );
|
|
} else {
|
|
$tagDisplay = '';
|
|
}
|
|
|
|
# Display the old title if the namespace/title has been changed
|
|
$oldTitleText = '';
|
|
$oldTitle = Title::makeTitle( $row->rc_namespace, $row->rc_title );
|
|
|
|
if ( !$title->equals( $oldTitle ) ) {
|
|
$oldTitleText = $oldTitle->getPrefixedText();
|
|
$oldTitleText = Html::rawElement(
|
|
'span',
|
|
[ 'class' => 'mw-newpages-oldtitle' ],
|
|
$this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
|
|
);
|
|
}
|
|
|
|
$ret = "{$time} {$plink} {$links} {$length} {$ulink} {$comment} "
|
|
. "{$tagDisplay} {$oldTitleText}";
|
|
|
|
// Let extensions add data
|
|
$this->hookRunner->onNewPagesLineEnding(
|
|
$this, $ret, $row, $classes, $attribs );
|
|
$attribs = array_filter( $attribs,
|
|
[ Sanitizer::class, 'isReservedDataAttribute' ],
|
|
ARRAY_FILTER_USE_KEY
|
|
);
|
|
|
|
if ( $classes ) {
|
|
$attribs['class'] = $classes;
|
|
}
|
|
|
|
return Html::rawElement( 'li', $attribs, $ret ) . "\n";
|
|
}
|
|
|
|
/**
|
|
* @param stdClass $result Result row from recent changes
|
|
* @param Title $title
|
|
* @return RevisionRecord
|
|
*/
|
|
protected function revisionFromRcResult( stdClass $result, Title $title ): RevisionRecord {
|
|
$revRecord = new MutableRevisionRecord( $title );
|
|
$revRecord->setTimestamp( $result->rc_timestamp );
|
|
$revRecord->setId( $result->rc_this_oldid );
|
|
$revRecord->setVisibility( (int)$result->rc_deleted );
|
|
|
|
$user = new UserIdentityValue(
|
|
(int)$result->rc_user,
|
|
$result->rc_user_text
|
|
);
|
|
$revRecord->setUser( $user );
|
|
|
|
return $revRecord;
|
|
}
|
|
|
|
protected function doBatchLookups() {
|
|
$linkBatch = $this->linkBatchFactory->newLinkBatch();
|
|
foreach ( $this->mResult as $row ) {
|
|
$linkBatch->add( NS_USER, $row->rc_user_text );
|
|
$linkBatch->add( NS_USER_TALK, $row->rc_user_text );
|
|
$linkBatch->add( $row->page_namespace, $row->page_title );
|
|
}
|
|
$linkBatch->execute();
|
|
|
|
$this->formattedComments = $this->rowCommentFormatter->formatRows(
|
|
$this->mResult, 'rc_comment', 'page_namespace', 'page_title', 'rc_id', true
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
protected function getStartBody() {
|
|
return "<section class='mw-pager-body'>\n";
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
protected function getEndBody() {
|
|
return "</section>\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retain the old class name for backwards compatibility.
|
|
* @deprecated since 1.41
|
|
*/
|
|
class_alias( NewPagesPager::class, 'NewPagesPager' );
|