Merge "Mocha tests: Support language links to en-x-piglatin"
[mediawiki.git] / includes / specials / pagers / NewPagesPager.php
blobb841430fb17a4ce26ae2cb6458869d140ad598b4
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
19 * @ingroup Pager
22 namespace MediaWiki\Pager;
24 use ChangesList;
25 use ChangeTags;
26 use MapCacheLRU;
27 use MediaWiki\Cache\LinkBatchFactory;
28 use MediaWiki\ChangeTags\ChangeTagsStore;
29 use MediaWiki\CommentFormatter\RowCommentFormatter;
30 use MediaWiki\Content\IContentHandlerFactory;
31 use MediaWiki\Context\IContextSource;
32 use MediaWiki\HookContainer\HookContainer;
33 use MediaWiki\HookContainer\HookRunner;
34 use MediaWiki\Html\FormOptions;
35 use MediaWiki\Html\Html;
36 use MediaWiki\Linker\Linker;
37 use MediaWiki\Linker\LinkRenderer;
38 use MediaWiki\Parser\Sanitizer;
39 use MediaWiki\Permissions\GroupPermissionsLookup;
40 use MediaWiki\Revision\MutableRevisionRecord;
41 use MediaWiki\Revision\RevisionRecord;
42 use MediaWiki\Title\NamespaceInfo;
43 use MediaWiki\Title\Title;
44 use MediaWiki\User\TempUser\TempUserConfig;
45 use MediaWiki\User\UserIdentityValue;
46 use RecentChange;
47 use stdClass;
48 use Wikimedia\Rdbms\IExpression;
50 /**
51 * @internal For use by SpecialNewPages
52 * @ingroup RecentChanges
53 * @ingroup Pager
55 class NewPagesPager extends ReverseChronologicalPager {
57 /**
58 * @var FormOptions
60 protected $opts;
62 protected MapCacheLRU $tagsCache;
64 /** @var string[] */
65 private $formattedComments = [];
66 /** @var bool Whether to group items by date by default this is disabled, but eventually the intention
67 * should be to default to true once all pages have been transitioned to support date grouping.
69 public $mGroupByDate = true;
71 private GroupPermissionsLookup $groupPermissionsLookup;
72 private HookRunner $hookRunner;
73 private LinkBatchFactory $linkBatchFactory;
74 private NamespaceInfo $namespaceInfo;
75 private ChangeTagsStore $changeTagsStore;
76 private RowCommentFormatter $rowCommentFormatter;
77 private IContentHandlerFactory $contentHandlerFactory;
78 private TempUserConfig $tempUserConfig;
80 /**
81 * @param IContextSource $context
82 * @param LinkRenderer $linkRenderer
83 * @param GroupPermissionsLookup $groupPermissionsLookup
84 * @param HookContainer $hookContainer
85 * @param LinkBatchFactory $linkBatchFactory
86 * @param NamespaceInfo $namespaceInfo
87 * @param ChangeTagsStore $changeTagsStore
88 * @param RowCommentFormatter $rowCommentFormatter
89 * @param IContentHandlerFactory $contentHandlerFactory
90 * @param TempUserConfig $tempUserConfig
91 * @param FormOptions $opts
93 public function __construct(
94 IContextSource $context,
95 LinkRenderer $linkRenderer,
96 GroupPermissionsLookup $groupPermissionsLookup,
97 HookContainer $hookContainer,
98 LinkBatchFactory $linkBatchFactory,
99 NamespaceInfo $namespaceInfo,
100 ChangeTagsStore $changeTagsStore,
101 RowCommentFormatter $rowCommentFormatter,
102 IContentHandlerFactory $contentHandlerFactory,
103 TempUserConfig $tempUserConfig,
104 FormOptions $opts
106 parent::__construct( $context, $linkRenderer );
107 $this->groupPermissionsLookup = $groupPermissionsLookup;
108 $this->hookRunner = new HookRunner( $hookContainer );
109 $this->linkBatchFactory = $linkBatchFactory;
110 $this->namespaceInfo = $namespaceInfo;
111 $this->changeTagsStore = $changeTagsStore;
112 $this->rowCommentFormatter = $rowCommentFormatter;
113 $this->contentHandlerFactory = $contentHandlerFactory;
114 $this->tempUserConfig = $tempUserConfig;
115 $this->opts = $opts;
116 $this->tagsCache = new MapCacheLRU( 50 );
119 public function getQueryInfo() {
120 $rcQuery = RecentChange::getQueryInfo();
122 $conds = [];
123 $conds['rc_new'] = 1;
125 $username = $this->opts->getValue( 'username' );
126 $user = Title::makeTitleSafe( NS_USER, $username );
128 $size = abs( intval( $this->opts->getValue( 'size' ) ) );
129 if ( $size > 0 ) {
130 $db = $this->getDatabase();
131 if ( $this->opts->getValue( 'size-mode' ) === 'max' ) {
132 $conds[] = $db->expr( 'page_len', '<=', $size );
133 } else {
134 $conds[] = $db->expr( 'page_len', '>=', $size );
138 if ( $user ) {
139 $conds['actor_name'] = $user->getText();
140 } elseif ( $this->opts->getValue( 'hideliu' ) ) {
141 // Only include anonymous users if the 'hideliu' option has been provided.
142 $anonOnlyExpr = $this->getDatabase()->expr( 'actor_user', '=', null );
143 if ( $this->tempUserConfig->isKnown() ) {
144 $anonOnlyExpr = $anonOnlyExpr->orExpr( $this->tempUserConfig->getMatchCondition(
145 $this->getDatabase(), 'actor_name', IExpression::LIKE
146 ) );
148 $conds[] = $anonOnlyExpr;
151 $conds = array_merge( $conds, $this->getNamespaceCond() );
153 # If this user cannot see patrolled edits or they are off, don't do dumb queries!
154 if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
155 $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
158 if ( $this->opts->getValue( 'hidebots' ) ) {
159 $conds['rc_bot'] = 0;
162 if ( $this->opts->getValue( 'hideredirs' ) ) {
163 $conds['page_is_redirect'] = 0;
166 // Allow changes to the New Pages query
167 $tables = array_merge( $rcQuery['tables'], [ 'page' ] );
168 $fields = array_merge( $rcQuery['fields'], [
169 'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title',
170 'page_content_model',
171 ] );
172 $join_conds = [ 'page' => [ 'JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
174 $this->hookRunner->onSpecialNewpagesConditions(
175 $this, $this->opts, $conds, $tables, $fields, $join_conds );
177 $info = [
178 'tables' => $tables,
179 'fields' => $fields,
180 'conds' => $conds,
181 'options' => [],
182 'join_conds' => $join_conds
185 // Modify query for tags
186 $this->changeTagsStore->modifyDisplayQuery(
187 $info['tables'],
188 $info['fields'],
189 $info['conds'],
190 $info['join_conds'],
191 $info['options'],
192 $this->opts['tagfilter'],
193 $this->opts['tagInvert']
196 return $info;
199 // Based on ContribsPager.php
200 private function getNamespaceCond() {
201 $namespace = $this->opts->getValue( 'namespace' );
202 if ( $namespace === 'all' || $namespace === '' ) {
203 return [];
206 $namespace = intval( $namespace );
207 if ( $namespace < NS_MAIN ) {
208 // Negative namespaces are invalid
209 return [];
212 $invert = $this->opts->getValue( 'invert' );
213 $associated = $this->opts->getValue( 'associated' );
215 $eq_op = $invert ? '!=' : '=';
216 $dbr = $this->getDatabase();
217 $namespaces = [ $namespace ];
218 if ( $associated ) {
219 $namespaces[] = $this->namespaceInfo->getAssociated( $namespace );
222 return [ $dbr->expr( 'rc_namespace', $eq_op, $namespaces ) ];
225 public function getIndexField() {
226 return [ [ 'rc_timestamp', 'rc_id' ] ];
229 public function formatRow( $row ) {
230 $title = Title::newFromRow( $row );
232 // Revision deletion works on revisions,
233 // so cast our recent change row to a revision row.
234 $revRecord = $this->revisionFromRcResult( $row, $title );
236 $classes = [];
237 $attribs = [ 'data-mw-revid' => $row->rc_this_oldid ];
239 $lang = $this->getLanguage();
240 $time = ChangesList::revDateLink( $revRecord, $this->getUser(), $lang, null, 'mw-newpages-time' );
242 $linkRenderer = $this->getLinkRenderer();
244 $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
246 $plink = Html::rawElement( 'bdi', [ 'dir' => $lang->getDir() ], $linkRenderer->makeKnownLink(
247 $title,
248 null,
249 [ 'class' => 'mw-newpages-pagename' ],
250 $query
251 ) );
252 $linkArr = [];
253 $linkArr[] = $linkRenderer->makeKnownLink(
254 $title,
255 $this->msg( 'hist' )->text(),
256 [ 'class' => 'mw-newpages-history' ],
257 [ 'action' => 'history' ]
259 if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
260 ->supportsDirectEditing()
262 $linkArr[] = $linkRenderer->makeKnownLink(
263 $title,
264 $this->msg( 'editlink' )->text(),
265 [ 'class' => 'mw-newpages-edit' ],
266 [ 'action' => 'edit' ]
269 $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
270 ->pipeList( $linkArr ) )->escaped();
272 $length = Html::rawElement(
273 'span',
274 [ 'class' => 'mw-newpages-length' ],
275 $this->msg( 'brackets' )->rawParams(
276 $this->msg( 'nbytes' )->numParams( $row->length )->escaped()
277 )->escaped()
280 $ulink = Linker::revUserTools( $revRecord );
281 $rc = RecentChange::newFromRow( $row );
282 if ( ChangesList::userCan( $rc, RevisionRecord::DELETED_COMMENT, $this->getAuthority() ) ) {
283 $comment = $this->formattedComments[$rc->mAttribs['rc_id']];
284 } else {
285 $comment = '<span class="comment">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
287 if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
288 $deletedClass = 'history-deleted';
289 if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
290 $deletedClass .= ' mw-history-suppressed';
292 $comment = '<span class="' . $deletedClass . ' comment">' . $comment . '</span>';
295 if ( $this->getUser()->useNPPatrol() && !$row->rc_patrolled ) {
296 $classes[] = 'not-patrolled';
299 # Add a class for zero byte pages
300 if ( $row->length == 0 ) {
301 $classes[] = 'mw-newpages-zero-byte-page';
304 # Tags, if any.
305 if ( isset( $row->ts_tags ) ) {
306 [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
307 $this->tagsCache->makeKey(
308 $row->ts_tags,
309 $this->getUser()->getName(),
310 $lang->getCode()
312 fn () => ChangeTags::formatSummaryRow(
313 $row->ts_tags,
314 'newpages',
315 $this->getContext()
318 $classes = array_merge( $classes, $newClasses );
319 } else {
320 $tagDisplay = '';
323 # Display the old title if the namespace/title has been changed
324 $oldTitleText = '';
325 $oldTitle = Title::makeTitle( $row->rc_namespace, $row->rc_title );
327 if ( !$title->equals( $oldTitle ) ) {
328 $oldTitleText = $oldTitle->getPrefixedText();
329 $oldTitleText = Html::rawElement(
330 'span',
331 [ 'class' => 'mw-newpages-oldtitle' ],
332 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
336 $ret = "{$time} {$plink} {$links} {$length} {$ulink} {$comment} "
337 . "{$tagDisplay} {$oldTitleText}";
339 // Let extensions add data
340 $this->hookRunner->onNewPagesLineEnding(
341 $this, $ret, $row, $classes, $attribs );
342 $attribs = array_filter( $attribs,
343 [ Sanitizer::class, 'isReservedDataAttribute' ],
344 ARRAY_FILTER_USE_KEY
347 if ( $classes ) {
348 $attribs['class'] = $classes;
351 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
355 * @param stdClass $result Result row from recent changes
356 * @param Title $title
357 * @return RevisionRecord
359 protected function revisionFromRcResult( stdClass $result, Title $title ): RevisionRecord {
360 $revRecord = new MutableRevisionRecord( $title );
361 $revRecord->setTimestamp( $result->rc_timestamp );
362 $revRecord->setId( $result->rc_this_oldid );
363 $revRecord->setVisibility( (int)$result->rc_deleted );
365 $user = new UserIdentityValue(
366 (int)$result->rc_user,
367 $result->rc_user_text
369 $revRecord->setUser( $user );
371 return $revRecord;
374 protected function doBatchLookups() {
375 $linkBatch = $this->linkBatchFactory->newLinkBatch();
376 foreach ( $this->mResult as $row ) {
377 $linkBatch->add( NS_USER, $row->rc_user_text );
378 $linkBatch->add( NS_USER_TALK, $row->rc_user_text );
379 $linkBatch->add( $row->page_namespace, $row->page_title );
381 $linkBatch->execute();
383 $this->formattedComments = $this->rowCommentFormatter->formatRows(
384 $this->mResult, 'rc_comment', 'page_namespace', 'page_title', 'rc_id', true
389 * @inheritDoc
391 protected function getStartBody() {
392 return "<section class='mw-pager-body'>\n";
396 * @inheritDoc
398 protected function getEndBody() {
399 return "</section>\n";
404 * Retain the old class name for backwards compatibility.
405 * @deprecated since 1.41
407 class_alias( NewPagesPager::class, 'NewPagesPager' );