MediaWiki REL1_37
SpecialNewpages.php
Go to the documentation of this file.
1<?php
34
44 protected $opts;
46 protected $customFilters;
47
48 protected $showNavigation = false;
49
52
55
58
61
64
67
70
73
84 public function __construct(
85 LinkBatchFactory $linkBatchFactory,
86 CommentStore $commentStore,
87 IContentHandlerFactory $contentHandlerFactory,
88 GroupPermissionsLookup $groupPermissionsLookup,
89 ILoadBalancer $loadBalancer,
90 RevisionLookup $revisionLookup,
91 NamespaceInfo $namespaceInfo,
93 ) {
94 parent::__construct( 'Newpages' );
95 $this->linkBatchFactory = $linkBatchFactory;
96 $this->commentStore = $commentStore;
97 $this->contentHandlerFactory = $contentHandlerFactory;
98 $this->groupPermissionsLookup = $groupPermissionsLookup;
99 $this->loadBalancer = $loadBalancer;
100 $this->revisionLookup = $revisionLookup;
101 $this->namespaceInfo = $namespaceInfo;
102 $this->userOptionsLookup = $userOptionsLookup;
103 }
104
108 protected function setup( $par ) {
109 $opts = new FormOptions();
110 $this->opts = $opts; // bind
111 $opts->add( 'hideliu', false );
112 $opts->add(
113 'hidepatrolled',
114 $this->userOptionsLookup->getBoolOption( $this->getUser(), 'newpageshidepatrolled' )
115 );
116 $opts->add( 'hidebots', false );
117 $opts->add( 'hideredirs', true );
118 $opts->add(
119 'limit',
120 $this->userOptionsLookup->getIntOption( $this->getUser(), 'rclimit' )
121 );
122 $opts->add( 'offset', '' );
123 $opts->add( 'namespace', '0' );
124 $opts->add( 'username', '' );
125 $opts->add( 'feed', '' );
126 $opts->add( 'tagfilter', '' );
127 $opts->add( 'invert', false );
128 $opts->add( 'associated', false );
129 $opts->add( 'size-mode', 'max' );
130 $opts->add( 'size', 0 );
131
132 $this->customFilters = [];
133 $this->getHookRunner()->onSpecialNewPagesFilters( $this, $this->customFilters );
134 // @phan-suppress-next-line PhanEmptyForeach False positive
135 foreach ( $this->customFilters as $key => $params ) {
136 $opts->add( $key, $params['default'] );
137 }
138
139 $opts->fetchValuesFromRequest( $this->getRequest() );
140 if ( $par ) {
141 $this->parseParams( $par );
142 }
143
144 $opts->validateIntBounds( 'limit', 0, 5000 );
145 }
146
150 protected function parseParams( $par ) {
151 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
152 foreach ( $bits as $bit ) {
153 if ( $bit === 'shownav' ) {
154 $this->showNavigation = true;
155 }
156 if ( $bit === 'hideliu' ) {
157 $this->opts->setValue( 'hideliu', true );
158 }
159 if ( $bit === 'hidepatrolled' ) {
160 $this->opts->setValue( 'hidepatrolled', true );
161 }
162 if ( $bit === 'hidebots' ) {
163 $this->opts->setValue( 'hidebots', true );
164 }
165 if ( $bit === 'showredirs' ) {
166 $this->opts->setValue( 'hideredirs', false );
167 }
168 if ( is_numeric( $bit ) ) {
169 $this->opts->setValue( 'limit', intval( $bit ) );
170 }
171
172 $m = [];
173 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
174 $this->opts->setValue( 'limit', intval( $m[1] ) );
175 }
176 // PG offsets not just digits!
177 if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
178 $this->opts->setValue( 'offset', intval( $m[1] ) );
179 }
180 if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
181 $this->opts->setValue( 'username', $m[1] );
182 }
183 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
184 $ns = $this->getLanguage()->getNsIndex( $m[1] );
185 if ( $ns !== false ) {
186 $this->opts->setValue( 'namespace', $ns );
187 }
188 }
189 }
190 }
191
197 public function execute( $par ) {
198 $out = $this->getOutput();
199
200 $this->setHeaders();
201 $this->outputHeader();
202
203 $this->showNavigation = !$this->including(); // Maybe changed in setup
204 $this->setup( $par );
205
206 $this->addHelpLink( 'Help:New pages' );
207
208 if ( !$this->including() ) {
209 // Settings
210 $this->form();
211
212 $feedType = $this->opts->getValue( 'feed' );
213 if ( $feedType ) {
214 $this->feed( $feedType );
215
216 return;
217 }
218
219 $allValues = $this->opts->getAllValues();
220 unset( $allValues['feed'] );
221 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
222 }
223
224 $pager = new NewPagesPager(
225 $this,
226 $this->opts,
227 $this->linkBatchFactory,
228 $this->getHookContainer(),
229 $this->groupPermissionsLookup,
230 $this->loadBalancer,
231 $this->namespaceInfo
232 );
233 $pager->mLimit = $this->opts->getValue( 'limit' );
234 $pager->mOffset = $this->opts->getValue( 'offset' );
235
236 if ( $pager->getNumRows() ) {
237 $navigation = '';
238 if ( $this->showNavigation ) {
239 $navigation = $pager->getNavigationBar();
240 }
241 $out->addHTML( $navigation . $pager->getBody() . $navigation );
242 // Add styles for change tags
243 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
244 } else {
245 $out->addWikiMsg( 'specialpage-empty' );
246 }
247 }
248
249 protected function filterLinks() {
250 // show/hide links
251 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
252
253 // Option value -> message mapping
254 $filters = [
255 'hideliu' => 'rcshowhideliu',
256 'hidepatrolled' => 'rcshowhidepatr',
257 'hidebots' => 'rcshowhidebots',
258 'hideredirs' => 'whatlinkshere-hideredirs'
259 ];
260 foreach ( $this->customFilters as $key => $params ) {
261 $filters[$key] = $params['msg'];
262 }
263
264 // Disable some if needed
265 if ( !$this->canAnonymousUsersCreatePages() ) {
266 unset( $filters['hideliu'] );
267 }
268 if ( !$this->getUser()->useNPPatrol() ) {
269 unset( $filters['hidepatrolled'] );
270 }
271
272 $links = [];
273 $changed = $this->opts->getChangedValues();
274 unset( $changed['offset'] ); // Reset offset if query type changes
275
276 // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
277 // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
278 // to 0 or 1.
279 // Also do this only for boolean options, not eg. namespace or tagfilter
280 foreach ( $changed as $key => $value ) {
281 if ( array_key_exists( $key, $filters ) ) {
282 $changed[$key] = $changed[$key] ? '1' : '0';
283 }
284 }
285
286 $self = $this->getPageTitle();
288 foreach ( $filters as $key => $msg ) {
289 $onoff = 1 - $this->opts->getValue( $key );
290 $link = $linkRenderer->makeLink(
291 $self,
292 new HtmlArmor( $showhide[$onoff] ),
293 [],
294 [ $key => $onoff ] + $changed
295 );
296 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
297 }
298
299 return $this->getLanguage()->pipeList( $links );
300 }
301
302 protected function form() {
303 $out = $this->getOutput();
304
305 // Consume values
306 $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
307 $namespace = $this->opts->consumeValue( 'namespace' );
308 $username = $this->opts->consumeValue( 'username' );
309 $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
310 $nsinvert = $this->opts->consumeValue( 'invert' );
311 $nsassociated = $this->opts->consumeValue( 'associated' );
312
313 $size = $this->opts->consumeValue( 'size' );
314 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
315
316 // Check username input validity
317 $ut = Title::makeTitleSafe( NS_USER, $username );
318 $userText = $ut ? $ut->getText() : '';
319
320 $formDescriptor = [
321 'namespace' => [
322 'type' => 'namespaceselect',
323 'name' => 'namespace',
324 'label-message' => 'namespace',
325 'default' => $namespace,
326 ],
327 'nsinvert' => [
328 'type' => 'check',
329 'name' => 'invert',
330 'label-message' => 'invert',
331 'default' => $nsinvert,
332 'tooltip' => 'invert',
333 ],
334 'nsassociated' => [
335 'type' => 'check',
336 'name' => 'associated',
337 'label-message' => 'namespace_association',
338 'default' => $nsassociated,
339 'tooltip' => 'namespace_association',
340 ],
341 'tagFilter' => [
342 'type' => 'tagfilter',
343 'name' => 'tagfilter',
344 'label-message' => 'tag-filter',
345 'default' => $tagFilterVal,
346 ],
347 'username' => [
348 'type' => 'user',
349 'name' => 'username',
350 'label-message' => 'newpages-username',
351 'default' => $userText,
352 'id' => 'mw-np-username',
353 'size' => 30,
354 ],
355 'size' => [
356 'type' => 'sizefilter',
357 'name' => 'size',
358 'default' => -$max * $size,
359 ],
360 ];
361
362 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
363
364 // Store query values in hidden fields so that form submission doesn't lose them
365 foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
366 $htmlForm->addHiddenField( $key, $value );
367 }
368
369 $htmlForm
370 ->setMethod( 'get' )
371 ->setFormIdentifier( 'newpagesform' )
372 // The form should be visible on each request (inclusive requests with submitted forms), so
373 // return always false here.
374 ->setSubmitCallback(
375 static function () {
376 return false;
377 }
378 )
379 ->setSubmitTextMsg( 'newpages-submit' )
380 ->setWrapperLegendMsg( 'newpages' )
381 ->addFooterText( Html::rawElement(
382 'div',
383 null,
384 $this->filterLinks()
385 ) )
386 ->show();
387 $out->addModuleStyles( 'mediawiki.special' );
388 }
389
395 private function revisionFromRcResult( stdClass $result, Title $title ): RevisionRecord {
396 $revRecord = new MutableRevisionRecord( $title );
397 $revRecord->setComment(
398 $this->commentStore->getComment( 'rc_comment', $result )
399 );
400 $revRecord->setVisibility( (int)$result->rc_deleted );
401
402 $user = new UserIdentityValue(
403 (int)$result->rc_user,
404 $result->rc_user_text
405 );
406 $revRecord->setUser( $user );
407
408 return $revRecord;
409 }
410
418 public function formatRow( $result ) {
419 $title = Title::newFromRow( $result );
420
421 // Revision deletion works on revisions,
422 // so cast our recent change row to a revision row.
423 $revRecord = $this->revisionFromRcResult( $result, $title );
424
425 $classes = [];
426 $attribs = [ 'data-mw-revid' => $result->rev_id ];
427
428 $lang = $this->getLanguage();
429 $dm = $lang->getDirMark();
430
431 $spanTime = Html::element( 'span', [ 'class' => 'mw-newpages-time' ],
432 $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
433 );
434 $linkRenderer = $this->getLinkRenderer();
435 $time = $linkRenderer->makeKnownLink(
436 $title,
437 new HtmlArmor( $spanTime ),
438 [],
439 [ 'oldid' => $result->rc_this_oldid ]
440 );
441
442 $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
443
444 $plink = $linkRenderer->makeKnownLink(
445 $title,
446 null,
447 [ 'class' => 'mw-newpages-pagename' ],
448 $query
449 );
450 $linkArr = [];
451 $linkArr[] = $linkRenderer->makeKnownLink(
452 $title,
453 $this->msg( 'hist' )->text(),
454 [ 'class' => 'mw-newpages-history' ],
455 [ 'action' => 'history' ]
456 );
457 if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
458 ->supportsDirectEditing()
459 ) {
460 $linkArr[] = $linkRenderer->makeKnownLink(
461 $title,
462 $this->msg( 'editlink' )->text(),
463 [ 'class' => 'mw-newpages-edit' ],
464 [ 'action' => 'edit' ]
465 );
466 }
467 $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
468 ->pipeList( $linkArr ) )->escaped();
469
470 $length = Html::rawElement(
471 'span',
472 [ 'class' => 'mw-newpages-length' ],
473 $this->msg( 'brackets' )->rawParams(
474 $this->msg( 'nbytes' )->numParams( $result->length )->escaped()
475 )->escaped()
476 );
477
478 $ulink = Linker::revUserTools( $revRecord );
479 $comment = Linker::revComment( $revRecord );
480
481 if ( $this->patrollable( $result ) ) {
482 $classes[] = 'not-patrolled';
483 }
484
485 # Add a class for zero byte pages
486 if ( $result->length == 0 ) {
487 $classes[] = 'mw-newpages-zero-byte-page';
488 }
489
490 # Tags, if any.
491 if ( isset( $result->ts_tags ) ) {
492 list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
493 $result->ts_tags,
494 'newpages',
495 $this->getContext()
496 );
497 $classes = array_merge( $classes, $newClasses );
498 } else {
499 $tagDisplay = '';
500 }
501
502 # Display the old title if the namespace/title has been changed
503 $oldTitleText = '';
504 $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
505
506 if ( !$title->equals( $oldTitle ) ) {
507 $oldTitleText = $oldTitle->getPrefixedText();
508 $oldTitleText = Html::rawElement(
509 'span',
510 [ 'class' => 'mw-newpages-oldtitle' ],
511 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
512 );
513 }
514
515 $ret = "{$time} {$dm}{$plink} {$links} {$dm}{$length} {$dm}{$ulink} {$comment} "
516 . "{$tagDisplay} {$oldTitleText}";
517
518 // Let extensions add data
519 $this->getHookRunner()->onNewPagesLineEnding(
520 $this, $ret, $result, $classes, $attribs );
521 $attribs = array_filter( $attribs,
522 [ Sanitizer::class, 'isReservedDataAttribute' ],
523 ARRAY_FILTER_USE_KEY
524 );
525
526 if ( $classes ) {
527 $attribs['class'] = $classes;
528 }
529
530 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
531 }
532
539 protected function patrollable( $result ) {
540 return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled );
541 }
542
548 protected function feed( $type ) {
549 if ( !$this->getConfig()->get( 'Feed' ) ) {
550 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
551
552 return;
553 }
554
555 $feedClasses = $this->getConfig()->get( 'FeedClasses' );
556 if ( !isset( $feedClasses[$type] ) ) {
557 $this->getOutput()->addWikiMsg( 'feed-invalid' );
558
559 return;
560 }
561
562 $feed = new $feedClasses[$type](
563 $this->feedTitle(),
564 $this->msg( 'tagline' )->text(),
565 $this->getPageTitle()->getFullURL()
566 );
567
568 $pager = new NewPagesPager(
569 $this,
570 $this->opts,
571 $this->linkBatchFactory,
572 $this->getHookContainer(),
573 $this->groupPermissionsLookup,
574 $this->loadBalancer,
575 $this->namespaceInfo
576 );
577 $limit = $this->opts->getValue( 'limit' );
578 $pager->mLimit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
579
580 $feed->outHeader();
581 if ( $pager->getNumRows() > 0 ) {
582 foreach ( $pager->mResult as $row ) {
583 $feed->outItem( $this->feedItem( $row ) );
584 }
585 }
586 $feed->outFooter();
587 }
588
589 protected function feedTitle() {
590 $desc = $this->getDescription();
591 $code = $this->getConfig()->get( 'LanguageCode' );
592 $sitename = $this->getConfig()->get( 'Sitename' );
593
594 return "$sitename - $desc [$code]";
595 }
596
597 protected function feedItem( $row ) {
598 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
599 if ( $title ) {
600 $date = $row->rc_timestamp;
601 $comments = $title->getTalkPage()->getFullURL();
602
603 return new FeedItem(
604 $title->getPrefixedText(),
605 $this->feedItemDesc( $row ),
606 $title->getFullURL(),
607 $date,
608 $this->feedItemAuthor( $row ),
609 $comments
610 );
611 } else {
612 return null;
613 }
614 }
615
616 protected function feedItemAuthor( $row ) {
617 return $row->rc_user_text ?? '';
618 }
619
620 protected function feedItemDesc( $row ) {
621 $revisionRecord = $this->revisionLookup->getRevisionById( $row->rev_id );
622 if ( !$revisionRecord ) {
623 return '';
624 }
625
626 $content = $revisionRecord->getContent( SlotRecord::MAIN );
627 if ( $content === null ) {
628 return '';
629 }
630
631 // XXX: include content model/type in feed item?
632 $revUser = $revisionRecord->getUser();
633 $revUserText = $revUser ? $revUser->getName() : '';
634 $revComment = $revisionRecord->getComment();
635 $revCommentText = $revComment ? $revComment->text : '';
636 return '<p>' . htmlspecialchars( $revUserText ) .
637 $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
638 htmlspecialchars( FeedItem::stripComment( $revCommentText ) ) .
639 "</p>\n<hr />\n<div>" .
640 nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
641 }
642
643 private function canAnonymousUsersCreatePages() {
644 return $this->groupPermissionsLookup->groupHasPermission( '*', 'createpage' ) ||
645 $this->groupPermissionsLookup->groupHasPermission( '*', 'createtalk' );
646 }
647
648 protected function getGroupName() {
649 return 'changes';
650 }
651
652 protected function getCacheTTL() {
653 return 60 * 5;
654 }
655}
UserOptionsLookup $userOptionsLookup
const NS_USER
Definition Defines.php:66
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
getContext()
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Handle database storage of comments such as edit summaries and log reasons.
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:33
Helper class to keep track of options when mixing links and form elements.
add( $name, $default, $type=self::AUTO)
Add an option to be handled by this FormOptions instance.
fetchValuesFromRequest(WebRequest $r, $optionKeys=null)
Fetch values for all options (or selected options) from the given WebRequest, making them available f...
validateIntBounds( $name, $min, $max)
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Shortcut to construct an includable special page.
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1782
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1319
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Page revision base class.
Value object representing a content slot associated with a page revision.
Value object representing a user's identity.
Provides access to user options.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
A special page that list newly created pages.
__construct(LinkBatchFactory $linkBatchFactory, CommentStore $commentStore, IContentHandlerFactory $contentHandlerFactory, GroupPermissionsLookup $groupPermissionsLookup, ILoadBalancer $loadBalancer, RevisionLookup $revisionLookup, NamespaceInfo $namespaceInfo, UserOptionsLookup $userOptionsLookup)
LinkBatchFactory $linkBatchFactory
ILoadBalancer $loadBalancer
GroupPermissionsLookup $groupPermissionsLookup
CommentStore $commentStore
NamespaceInfo $namespaceInfo
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
patrollable( $result)
Should a specific result row provide "patrollable" links?
RevisionLookup $revisionLookup
feed( $type)
Output a subscription feed listing recent edits to this page.
execute( $par)
Show a form for filtering namespace and username.
IContentHandlerFactory $contentHandlerFactory
UserOptionsLookup $userOptionsLookup
formatRow( $result)
Format a row, providing the timestamp, links to the page/history, size, user links,...
revisionFromRcResult(stdClass $result, Title $title)
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
LinkRenderer null $linkRenderer
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getRequest()
Get the WebRequest being used for this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getLanguage()
Shortcut to get user's language.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
including( $x=null)
Whether the special page is being evaluated via transclusion.
Represents a title within MediaWiki.
Definition Title.php:48
Service for looking up page revisions.
Database cluster connection, tracking, load balancing, and transaction manager interface.
$content
Definition router.php:76
if(!isset( $args[0])) $lang