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