Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.75% covered (warning)
80.75%
151 / 187
66.67% covered (warning)
66.67%
6 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewPagesPager
81.18% covered (warning)
81.18%
151 / 186
66.67% covered (warning)
66.67%
6 / 9
43.16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getQueryInfo
88.46% covered (warning)
88.46%
46 / 52
0.00% covered (danger)
0.00%
0 / 1
10.15
 getNamespaceCond
85.71% covered (warning)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
6.10
 getIndexField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatRow
68.97% covered (warning)
68.97%
60 / 87
0.00% covered (danger)
0.00%
0 / 1
16.30
 revisionFromRcResult
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 doBatchLookups
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getStartBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEndBody
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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.
7 *
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.
12 *
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
17 *
18 * @file
19 * @ingroup Pager
20 */
21
22namespace MediaWiki\Pager;
23
24use ChangesList;
25use ChangeTags;
26use MapCacheLRU;
27use MediaWiki\Cache\LinkBatchFactory;
28use MediaWiki\ChangeTags\ChangeTagsStore;
29use MediaWiki\CommentFormatter\RowCommentFormatter;
30use MediaWiki\Content\IContentHandlerFactory;
31use MediaWiki\Context\IContextSource;
32use MediaWiki\HookContainer\HookContainer;
33use MediaWiki\HookContainer\HookRunner;
34use MediaWiki\Html\FormOptions;
35use MediaWiki\Html\Html;
36use MediaWiki\Linker\Linker;
37use MediaWiki\Linker\LinkRenderer;
38use MediaWiki\Parser\Sanitizer;
39use MediaWiki\Permissions\GroupPermissionsLookup;
40use MediaWiki\Revision\MutableRevisionRecord;
41use MediaWiki\Revision\RevisionRecord;
42use MediaWiki\Title\NamespaceInfo;
43use MediaWiki\Title\Title;
44use MediaWiki\User\TempUser\TempUserConfig;
45use MediaWiki\User\UserIdentityValue;
46use RecentChange;
47use stdClass;
48use Wikimedia\Rdbms\IExpression;
49
50/**
51 * @internal For use by SpecialNewPages
52 * @ingroup RecentChanges
53 * @ingroup Pager
54 */
55class NewPagesPager extends ReverseChronologicalPager {
56
57    /**
58     * @var FormOptions
59     */
60    protected $opts;
61
62    protected MapCacheLRU $tagsCache;
63
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.
68     */
69    public $mGroupByDate = true;
70
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;
79
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
92     */
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
105    ) {
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 );
117    }
118
119    public function getQueryInfo() {
120        $rcQuery = RecentChange::getQueryInfo();
121
122        $conds = [];
123        $conds['rc_new'] = 1;
124
125        $username = $this->opts->getValue( 'username' );
126        $user = Title::makeTitleSafe( NS_USER, $username );
127
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 );
135            }
136        }
137
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                ) );
147            }
148            $conds[] = $anonOnlyExpr;
149        }
150
151        $conds = array_merge( $conds, $this->getNamespaceCond() );
152
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;
156        }
157
158        if ( $this->opts->getValue( 'hidebots' ) ) {
159            $conds['rc_bot'] = 0;
160        }
161
162        if ( $this->opts->getValue( 'hideredirs' ) ) {
163            $conds['page_is_redirect'] = 0;
164        }
165
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'];
173
174        $this->hookRunner->onSpecialNewpagesConditions(
175            $this, $this->opts, $conds, $tables, $fields, $join_conds );
176
177        $info = [
178            'tables' => $tables,
179            'fields' => $fields,
180            'conds' => $conds,
181            'options' => [],
182            'join_conds' => $join_conds
183        ];
184
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']
194        );
195
196        return $info;
197    }
198
199    // Based on ContribsPager.php
200    private function getNamespaceCond() {
201        $namespace = $this->opts->getValue( 'namespace' );
202        if ( $namespace === 'all' || $namespace === '' ) {
203            return [];
204        }
205
206        $namespace = intval( $namespace );
207        if ( $namespace < NS_MAIN ) {
208            // Negative namespaces are invalid
209            return [];
210        }
211
212        $invert = $this->opts->getValue( 'invert' );
213        $associated = $this->opts->getValue( 'associated' );
214
215        $eq_op = $invert ? '!=' : '=';
216        $dbr = $this->getDatabase();
217        $namespaces = [ $namespace ];
218        if ( $associated ) {
219            $namespaces[] = $this->namespaceInfo->getAssociated( $namespace );
220        }
221
222        return [ $dbr->expr( 'rc_namespace', $eq_op, $namespaces ) ];
223    }
224
225    public function getIndexField() {
226        return [ [ 'rc_timestamp', 'rc_id' ] ];
227    }
228
229    public function formatRow( $row ) {
230        $title = Title::newFromRow( $row );
231
232        // Revision deletion works on revisions,
233        // so cast our recent change row to a revision row.
234        $revRecord = $this->revisionFromRcResult( $row, $title );
235
236        $classes = [];
237        $attribs = [ 'data-mw-revid' => $row->rc_this_oldid ];
238
239        $lang = $this->getLanguage();
240        $time = ChangesList::revDateLink( $revRecord, $this->getUser(), $lang, null, 'mw-newpages-time' );
241
242        $linkRenderer = $this->getLinkRenderer();
243
244        $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
245
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' ]
258        );
259        if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
260            ->supportsDirectEditing()
261        ) {
262            $linkArr[] = $linkRenderer->makeKnownLink(
263                $title,
264                $this->msg( 'editlink' )->text(),
265                [ 'class' => 'mw-newpages-edit' ],
266                [ 'action' => 'edit' ]
267            );
268        }
269        $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
270            ->pipeList( $linkArr ) )->escaped();
271
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()
278        );
279
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>';
286        }
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';
291            }
292            $comment = '<span class="' . $deletedClass . ' comment">' . $comment . '</span>';
293        }
294
295        if ( $this->getUser()->useNPPatrol() && !$row->rc_patrolled ) {
296            $classes[] = 'not-patrolled';
297        }
298
299        # Add a class for zero byte pages
300        if ( $row->length == 0 ) {
301            $classes[] = 'mw-newpages-zero-byte-page';
302        }
303
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()
311                ),
312                fn () => ChangeTags::formatSummaryRow(
313                    $row->ts_tags,
314                    'newpages',
315                    $this->getContext()
316                )
317            );
318            $classes = array_merge( $classes, $newClasses );
319        } else {
320            $tagDisplay = '';
321        }
322
323        # Display the old title if the namespace/title has been changed
324        $oldTitleText = '';
325        $oldTitle = Title::makeTitle( $row->rc_namespace, $row->rc_title );
326
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()
333            );
334        }
335
336        $ret = "{$time} {$plink} {$links} {$length} {$ulink} {$comment} "
337            . "{$tagDisplay} {$oldTitleText}";
338
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
345        );
346
347        if ( $classes ) {
348            $attribs['class'] = $classes;
349        }
350
351        return Html::rawElement( 'li', $attribs, $ret ) . "\n";
352    }
353
354    /**
355     * @param stdClass $result Result row from recent changes
356     * @param Title $title
357     * @return RevisionRecord
358     */
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 );
364
365        $user = new UserIdentityValue(
366            (int)$result->rc_user,
367            $result->rc_user_text
368        );
369        $revRecord->setUser( $user );
370
371        return $revRecord;
372    }
373
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 );
380        }
381        $linkBatch->execute();
382
383        $this->formattedComments = $this->rowCommentFormatter->formatRows(
384            $this->mResult, 'rc_comment', 'page_namespace', 'page_title', 'rc_id', true
385        );
386    }
387
388    /**
389     * @inheritDoc
390     */
391    protected function getStartBody() {
392        return "<section class='mw-pager-body'>\n";
393    }
394
395    /**
396     * @inheritDoc
397     */
398    protected function getEndBody() {
399        return "</section>\n";
400    }
401}
402
403/**
404 * Retain the old class name for backwards compatibility.
405 * @deprecated since 1.41
406 */
407class_alias( NewPagesPager::class, 'NewPagesPager' );