Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.75% |
151 / 187 |
|
66.67% |
6 / 9 |
CRAP | |
0.00% |
0 / 1 |
NewPagesPager | |
81.18% |
151 / 186 |
|
66.67% |
6 / 9 |
43.16 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
getQueryInfo | |
88.46% |
46 / 52 |
|
0.00% |
0 / 1 |
10.15 | |||
getNamespaceCond | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
6.10 | |||
getIndexField | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatRow | |
68.97% |
60 / 87 |
|
0.00% |
0 / 1 |
16.30 | |||
revisionFromRcResult | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
doBatchLookups | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getStartBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEndBody | |
100.00% |
1 / 1 |
|
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 | |
22 | namespace MediaWiki\Pager; |
23 | |
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; |
49 | |
50 | /** |
51 | * @internal For use by SpecialNewPages |
52 | * @ingroup RecentChanges |
53 | * @ingroup Pager |
54 | */ |
55 | class 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 | */ |
407 | class_alias( NewPagesPager::class, 'NewPagesPager' ); |