Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
NewPagesPager
0.00% covered (danger)
0.00%
0 / 190
0.00% covered (danger)
0.00%
0 / 10
1482
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
110
 canAnonymousUsersCreatePages
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getNamespaceCond
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatRow
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 1
156
 revisionFromRcResult
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getStartBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEndBody
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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\UserIdentityValue;
45use RecentChange;
46use stdClass;
47
48/**
49 * @internal For use by SpecialNewPages
50 * @ingroup Pager
51 */
52class NewPagesPager extends ReverseChronologicalPager {
53
54    /**
55     * @var FormOptions
56     */
57    protected $opts;
58
59    protected MapCacheLRU $tagsCache;
60
61    /** @var string[] */
62    private $formattedComments = [];
63    /** @var bool Whether to group items by date by default this is disabled, but eventually the intention
64     * should be to default to true once all pages have been transitioned to support date grouping.
65     */
66    public $mGroupByDate = true;
67
68    private GroupPermissionsLookup $groupPermissionsLookup;
69    private HookRunner $hookRunner;
70    private LinkBatchFactory $linkBatchFactory;
71    private NamespaceInfo $namespaceInfo;
72    private ChangeTagsStore $changeTagsStore;
73    private RowCommentFormatter $rowCommentFormatter;
74    private IContentHandlerFactory $contentHandlerFactory;
75
76    /**
77     * @param IContextSource $context
78     * @param LinkRenderer $linkRenderer
79     * @param GroupPermissionsLookup $groupPermissionsLookup
80     * @param HookContainer $hookContainer
81     * @param LinkBatchFactory $linkBatchFactory
82     * @param NamespaceInfo $namespaceInfo
83     * @param ChangeTagsStore $changeTagsStore
84     * @param RowCommentFormatter $rowCommentFormatter
85     * @param IContentHandlerFactory $contentHandlerFactory
86     * @param FormOptions $opts
87     */
88    public function __construct(
89        IContextSource $context,
90        LinkRenderer $linkRenderer,
91        GroupPermissionsLookup $groupPermissionsLookup,
92        HookContainer $hookContainer,
93        LinkBatchFactory $linkBatchFactory,
94        NamespaceInfo $namespaceInfo,
95        ChangeTagsStore $changeTagsStore,
96        RowCommentFormatter $rowCommentFormatter,
97        IContentHandlerFactory $contentHandlerFactory,
98        FormOptions $opts
99    ) {
100        parent::__construct( $context, $linkRenderer );
101        $this->groupPermissionsLookup = $groupPermissionsLookup;
102        $this->hookRunner = new HookRunner( $hookContainer );
103        $this->linkBatchFactory = $linkBatchFactory;
104        $this->namespaceInfo = $namespaceInfo;
105        $this->changeTagsStore = $changeTagsStore;
106        $this->rowCommentFormatter = $rowCommentFormatter;
107        $this->contentHandlerFactory = $contentHandlerFactory;
108        $this->opts = $opts;
109        $this->tagsCache = new MapCacheLRU( 50 );
110    }
111
112    public function getQueryInfo() {
113        $rcQuery = RecentChange::getQueryInfo();
114
115        $conds = [];
116        $conds['rc_new'] = 1;
117
118        $username = $this->opts->getValue( 'username' );
119        $user = Title::makeTitleSafe( NS_USER, $username );
120
121        $size = abs( intval( $this->opts->getValue( 'size' ) ) );
122        if ( $size > 0 ) {
123            if ( $this->opts->getValue( 'size-mode' ) === 'max' ) {
124                $conds[] = 'page_len <= ' . $size;
125            } else {
126                $conds[] = 'page_len >= ' . $size;
127            }
128        }
129
130        if ( $user ) {
131            $conds['actor_name'] = $user->getText();
132        } elseif ( $this->canAnonymousUsersCreatePages() && $this->opts->getValue( 'hideliu' ) ) {
133            # If anons cannot make new pages, don't "exclude logged in users"!
134            $conds['actor_user'] = null;
135        }
136
137        $conds = array_merge( $conds, $this->getNamespaceCond() );
138
139        # If this user cannot see patrolled edits or they are off, don't do dumb queries!
140        if ( $this->opts->getValue( 'hidepatrolled' ) && $this->getUser()->useNPPatrol() ) {
141            $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED;
142        }
143
144        if ( $this->opts->getValue( 'hidebots' ) ) {
145            $conds['rc_bot'] = 0;
146        }
147
148        if ( $this->opts->getValue( 'hideredirs' ) ) {
149            $conds['page_is_redirect'] = 0;
150        }
151
152        // Allow changes to the New Pages query
153        $tables = array_merge( $rcQuery['tables'], [ 'page' ] );
154        $fields = array_merge( $rcQuery['fields'], [
155            'length' => 'page_len', 'rev_id' => 'page_latest', 'page_namespace', 'page_title',
156            'page_content_model',
157        ] );
158        $join_conds = [ 'page' => [ 'JOIN', 'page_id=rc_cur_id' ] ] + $rcQuery['joins'];
159
160        $this->hookRunner->onSpecialNewpagesConditions(
161            $this, $this->opts, $conds, $tables, $fields, $join_conds );
162
163        $info = [
164            'tables' => $tables,
165            'fields' => $fields,
166            'conds' => $conds,
167            'options' => [],
168            'join_conds' => $join_conds
169        ];
170
171        // Modify query for tags
172        $this->changeTagsStore->modifyDisplayQuery(
173            $info['tables'],
174            $info['fields'],
175            $info['conds'],
176            $info['join_conds'],
177            $info['options'],
178            $this->opts['tagfilter'],
179            $this->opts['tagInvert']
180        );
181
182        return $info;
183    }
184
185    private function canAnonymousUsersCreatePages() {
186        return $this->groupPermissionsLookup->groupHasPermission( '*', 'createpage' ) ||
187            $this->groupPermissionsLookup->groupHasPermission( '*', 'createtalk' );
188    }
189
190    // Based on ContribsPager.php
191    private function getNamespaceCond() {
192        $namespace = $this->opts->getValue( 'namespace' );
193        if ( $namespace === 'all' || $namespace === '' ) {
194            return [];
195        }
196
197        $namespace = intval( $namespace );
198        if ( $namespace < NS_MAIN ) {
199            // Negative namespaces are invalid
200            return [];
201        }
202
203        $invert = $this->opts->getValue( 'invert' );
204        $associated = $this->opts->getValue( 'associated' );
205
206        $eq_op = $invert ? '!=' : '=';
207        $bool_op = $invert ? 'AND' : 'OR';
208
209        $dbr = $this->getDatabase();
210        $selectedNS = $dbr->addQuotes( $namespace );
211        if ( !$associated ) {
212            return [ "rc_namespace $eq_op $selectedNS" ];
213        }
214
215        $associatedNS = $dbr->addQuotes(
216            $this->namespaceInfo->getAssociated( $namespace )
217        );
218        return [
219            "rc_namespace $eq_op $selectedNS " .
220            $bool_op .
221            " rc_namespace $eq_op $associatedNS"
222        ];
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->rev_id ];
238
239        $lang = $this->getLanguage();
240        $dm = $lang->getDirMark();
241        $time = ChangesList::revDateLink( $revRecord, $this->getUser(), $lang, null, 'mw-newpages-time' );
242
243        $linkRenderer = $this->getLinkRenderer();
244
245        $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
246
247        $plink = $linkRenderer->makeKnownLink(
248            $title,
249            null,
250            [ 'class' => 'mw-newpages-pagename' ],
251            $query
252        );
253        $linkArr = [];
254        $linkArr[] = $linkRenderer->makeKnownLink(
255            $title,
256            $this->msg( 'hist' )->text(),
257            [ 'class' => 'mw-newpages-history' ],
258            [ 'action' => 'history' ]
259        );
260        if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
261            ->supportsDirectEditing()
262        ) {
263            $linkArr[] = $linkRenderer->makeKnownLink(
264                $title,
265                $this->msg( 'editlink' )->text(),
266                [ 'class' => 'mw-newpages-edit' ],
267                [ 'action' => 'edit' ]
268            );
269        }
270        $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
271            ->pipeList( $linkArr ) )->escaped();
272
273        $length = Html::rawElement(
274            'span',
275            [ 'class' => 'mw-newpages-length' ],
276            $this->msg( 'brackets' )->rawParams(
277                $this->msg( 'nbytes' )->numParams( $row->length )->escaped()
278            )->escaped()
279        );
280
281        $ulink = Linker::revUserTools( $revRecord );
282        $rc = RecentChange::newFromRow( $row );
283        if ( ChangesList::userCan( $rc, RevisionRecord::DELETED_COMMENT, $this->getAuthority() ) ) {
284            $comment = $this->formattedComments[$rc->mAttribs['rc_id']];
285        } else {
286            $comment = '<span class="comment">' . $this->msg( 'rev-deleted-comment' )->escaped() . '</span>';
287        }
288        if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_COMMENT ) ) {
289            $deletedClass = 'history-deleted';
290            if ( ChangesList::isDeleted( $rc, RevisionRecord::DELETED_RESTRICTED ) ) {
291                $deletedClass .= ' mw-history-suppressed';
292            }
293            $comment = '<span class="' . $deletedClass . ' comment">' . $comment . '</span>';
294        }
295
296        if ( $this->getUser()->useNPPatrol() && !$row->rc_patrolled ) {
297            $classes[] = 'not-patrolled';
298        }
299
300        # Add a class for zero byte pages
301        if ( $row->length == 0 ) {
302            $classes[] = 'mw-newpages-zero-byte-page';
303        }
304
305        # Tags, if any.
306        if ( isset( $row->ts_tags ) ) {
307            [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback(
308                $this->tagsCache->makeKey(
309                    $row->ts_tags,
310                    $this->getUser()->getName(),
311                    $lang->getCode()
312                ),
313                fn () => ChangeTags::formatSummaryRow(
314                    $row->ts_tags,
315                    'newpages',
316                    $this->getContext()
317                )
318            );
319            $classes = array_merge( $classes, $newClasses );
320        } else {
321            $tagDisplay = '';
322        }
323
324        # Display the old title if the namespace/title has been changed
325        $oldTitleText = '';
326        $oldTitle = Title::makeTitle( $row->rc_namespace, $row->rc_title );
327
328        if ( !$title->equals( $oldTitle ) ) {
329            $oldTitleText = $oldTitle->getPrefixedText();
330            $oldTitleText = Html::rawElement(
331                'span',
332                [ 'class' => 'mw-newpages-oldtitle' ],
333                $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
334            );
335        }
336
337        $ret = "{$time} {$dm}{$plink} {$links} {$dm}{$length} {$dm}{$ulink} {$comment} "
338            . "{$tagDisplay} {$oldTitleText}";
339
340        // Let extensions add data
341        $this->hookRunner->onNewPagesLineEnding(
342            $this, $ret, $row, $classes, $attribs );
343        $attribs = array_filter( $attribs,
344            [ Sanitizer::class, 'isReservedDataAttribute' ],
345            ARRAY_FILTER_USE_KEY
346        );
347
348        if ( $classes ) {
349            $attribs['class'] = $classes;
350        }
351
352        return Html::rawElement( 'li', $attribs, $ret ) . "\n";
353    }
354
355    /**
356     * @param stdClass $result Result row from recent changes
357     * @param Title $title
358     * @return RevisionRecord
359     */
360    protected function revisionFromRcResult( stdClass $result, Title $title ): RevisionRecord {
361        $revRecord = new MutableRevisionRecord( $title );
362        $revRecord->setTimestamp( $result->rc_timestamp );
363        $revRecord->setId( $result->rc_this_oldid );
364        $revRecord->setVisibility( (int)$result->rc_deleted );
365
366        $user = new UserIdentityValue(
367            (int)$result->rc_user,
368            $result->rc_user_text
369        );
370        $revRecord->setUser( $user );
371
372        return $revRecord;
373    }
374
375    protected function doBatchLookups() {
376        $linkBatch = $this->linkBatchFactory->newLinkBatch();
377        foreach ( $this->mResult as $row ) {
378            $linkBatch->add( NS_USER, $row->rc_user_text );
379            $linkBatch->add( NS_USER_TALK, $row->rc_user_text );
380            $linkBatch->add( $row->page_namespace, $row->page_title );
381        }
382        $linkBatch->execute();
383
384        $this->formattedComments = $this->rowCommentFormatter->formatRows(
385            $this->mResult, 'rc_comment', 'page_namespace', 'page_title', 'rc_id', true
386        );
387    }
388
389    /**
390     * @inheritDoc
391     */
392    protected function getStartBody() {
393        return "<section class='mw-pager-body'>\n";
394    }
395
396    /**
397     * @inheritDoc
398     */
399    protected function getEndBody() {
400        return "</section>\n";
401    }
402}
403
404/**
405 * Retain the old class name for backwards compatibility.
406 * @deprecated since 1.41
407 */
408class_alias( NewPagesPager::class, 'NewPagesPager' );