Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 197 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
NewPagesPager | |
0.00% |
0 / 196 |
|
0.00% |
0 / 10 |
1482 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
110 | |||
canAnonymousUsersCreatePages | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getNamespaceCond | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
56 | |||
getIndexField | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatRow | |
0.00% |
0 / 96 |
|
0.00% |
0 / 1 |
156 | |||
revisionFromRcResult | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
doBatchLookups | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getStartBody | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getEndBody | |
0.00% |
0 / 1 |
|
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 | |
22 | namespace MediaWiki\Pager; |
23 | |
24 | use ChangesList; |
25 | use ChangeTags; |
26 | use HtmlArmor; |
27 | use MapCacheLRU; |
28 | use MediaWiki\Cache\LinkBatchFactory; |
29 | use MediaWiki\ChangeTags\ChangeTagsStore; |
30 | use MediaWiki\CommentFormatter\RowCommentFormatter; |
31 | use MediaWiki\Content\IContentHandlerFactory; |
32 | use MediaWiki\Context\IContextSource; |
33 | use MediaWiki\HookContainer\HookContainer; |
34 | use MediaWiki\HookContainer\HookRunner; |
35 | use MediaWiki\Html\FormOptions; |
36 | use MediaWiki\Html\Html; |
37 | use MediaWiki\Linker\Linker; |
38 | use MediaWiki\Linker\LinkRenderer; |
39 | use MediaWiki\Parser\Sanitizer; |
40 | use MediaWiki\Permissions\GroupPermissionsLookup; |
41 | use MediaWiki\Revision\MutableRevisionRecord; |
42 | use MediaWiki\Revision\RevisionRecord; |
43 | use MediaWiki\Title\NamespaceInfo; |
44 | use MediaWiki\Title\Title; |
45 | use MediaWiki\User\UserIdentityValue; |
46 | use RecentChange; |
47 | use stdClass; |
48 | |
49 | /** |
50 | * @internal For use by SpecialNewPages |
51 | * @ingroup Pager |
52 | */ |
53 | class 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 | */ |
405 | class_alias( NewPagesPager::class, 'NewPagesPager' ); |