3 * Implements Special:Newpages
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
21 * @ingroup SpecialPage
24 use MediaWiki\MediaWikiServices
;
27 * A special page that list newly created pages
29 * @ingroup SpecialPage
31 class SpecialNewpages
extends IncludableSpecialPage
{
36 protected $customFilters;
38 protected $showNavigation = false;
40 public function __construct() {
41 parent
::__construct( 'Newpages' );
45 * @param string|null $par
47 protected function setup( $par ) {
48 $opts = new FormOptions();
49 $this->opts
= $opts; // bind
50 $opts->add( 'hideliu', false );
51 $opts->add( 'hidepatrolled', $this->getUser()->getBoolOption( 'newpageshidepatrolled' ) );
52 $opts->add( 'hidebots', false );
53 $opts->add( 'hideredirs', true );
54 $opts->add( 'limit', $this->getUser()->getIntOption( 'rclimit' ) );
55 $opts->add( 'offset', '' );
56 $opts->add( 'namespace', '0' );
57 $opts->add( 'username', '' );
58 $opts->add( 'feed', '' );
59 $opts->add( 'tagfilter', '' );
60 $opts->add( 'invert', false );
61 $opts->add( 'associated', false );
62 $opts->add( 'size-mode', 'max' );
63 $opts->add( 'size', 0 );
65 $this->customFilters
= [];
66 Hooks
::run( 'SpecialNewPagesFilters', [ $this, &$this->customFilters
] );
67 foreach ( $this->customFilters
as $key => $params ) {
68 $opts->add( $key, $params['default'] );
71 $opts->fetchValuesFromRequest( $this->getRequest() );
73 $this->parseParams( $par );
76 $opts->validateIntBounds( 'limit', 0, 5000 );
82 protected function parseParams( $par ) {
83 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
84 foreach ( $bits as $bit ) {
85 if ( $bit === 'shownav' ) {
86 $this->showNavigation
= true;
88 if ( $bit === 'hideliu' ) {
89 $this->opts
->setValue( 'hideliu', true );
91 if ( $bit === 'hidepatrolled' ) {
92 $this->opts
->setValue( 'hidepatrolled', true );
94 if ( $bit === 'hidebots' ) {
95 $this->opts
->setValue( 'hidebots', true );
97 if ( $bit === 'showredirs' ) {
98 $this->opts
->setValue( 'hideredirs', false );
100 if ( is_numeric( $bit ) ) {
101 $this->opts
->setValue( 'limit', intval( $bit ) );
105 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
106 $this->opts
->setValue( 'limit', intval( $m[1] ) );
108 // PG offsets not just digits!
109 if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
110 $this->opts
->setValue( 'offset', intval( $m[1] ) );
112 if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
113 $this->opts
->setValue( 'username', $m[1] );
115 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
116 $ns = $this->getLanguage()->getNsIndex( $m[1] );
117 if ( $ns !== false ) {
118 $this->opts
->setValue( 'namespace', $ns );
125 * Show a form for filtering namespace and username
127 * @param string|null $par
129 public function execute( $par ) {
130 $out = $this->getOutput();
133 $this->outputHeader();
135 $this->showNavigation
= !$this->including(); // Maybe changed in setup
136 $this->setup( $par );
138 $this->addHelpLink( 'Help:New pages' );
140 if ( !$this->including() ) {
144 $feedType = $this->opts
->getValue( 'feed' );
146 $this->feed( $feedType );
151 $allValues = $this->opts
->getAllValues();
152 unset( $allValues['feed'] );
153 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
156 $pager = new NewPagesPager( $this, $this->opts
);
157 $pager->mLimit
= $this->opts
->getValue( 'limit' );
158 $pager->mOffset
= $this->opts
->getValue( 'offset' );
160 if ( $pager->getNumRows() ) {
162 if ( $this->showNavigation
) {
163 $navigation = $pager->getNavigationBar();
165 $out->addHTML( $navigation . $pager->getBody() . $navigation );
166 // Add styles for change tags
167 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
169 $out->addWikiMsg( 'specialpage-empty' );
173 protected function filterLinks() {
175 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
177 // Option value -> message mapping
179 'hideliu' => 'rcshowhideliu',
180 'hidepatrolled' => 'rcshowhidepatr',
181 'hidebots' => 'rcshowhidebots',
182 'hideredirs' => 'whatlinkshere-hideredirs'
184 foreach ( $this->customFilters
as $key => $params ) {
185 $filters[$key] = $params['msg'];
188 // Disable some if needed
189 if ( !MediaWikiServices
::getInstance()->getPermissionManager()
190 ->groupHasPermission( '*', 'createpage' )
192 unset( $filters['hideliu'] );
194 if ( !$this->getUser()->useNPPatrol() ) {
195 unset( $filters['hidepatrolled'] );
199 $changed = $this->opts
->getChangedValues();
200 unset( $changed['offset'] ); // Reset offset if query type changes
202 // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
203 // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
205 // Also do this only for boolean options, not eg. namespace or tagfilter
206 foreach ( $changed as $key => $value ) {
207 if ( array_key_exists( $key, $filters ) ) {
208 $changed[$key] = $changed[$key] ?
'1' : '0';
212 $self = $this->getPageTitle();
213 $linkRenderer = $this->getLinkRenderer();
214 foreach ( $filters as $key => $msg ) {
215 $onoff = 1 - $this->opts
->getValue( $key );
216 $link = $linkRenderer->makeLink(
218 new HtmlArmor( $showhide[$onoff] ),
220 [ $key => $onoff ] +
$changed
222 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
225 return $this->getLanguage()->pipeList( $links );
228 protected function form() {
229 $out = $this->getOutput();
232 $this->opts
->consumeValue( 'offset' ); // don't carry offset, DWIW
233 $namespace = $this->opts
->consumeValue( 'namespace' );
234 $username = $this->opts
->consumeValue( 'username' );
235 $tagFilterVal = $this->opts
->consumeValue( 'tagfilter' );
236 $nsinvert = $this->opts
->consumeValue( 'invert' );
237 $nsassociated = $this->opts
->consumeValue( 'associated' );
239 $size = $this->opts
->consumeValue( 'size' );
240 $max = $this->opts
->consumeValue( 'size-mode' ) === 'max';
242 // Check username input validity
243 $ut = Title
::makeTitleSafe( NS_USER
, $username );
244 $userText = $ut ?
$ut->getText() : '';
248 'type' => 'namespaceselect',
249 'name' => 'namespace',
250 'label-message' => 'namespace',
251 'default' => $namespace,
256 'label-message' => 'invert',
257 'default' => $nsinvert,
258 'tooltip' => 'invert',
262 'name' => 'associated',
263 'label-message' => 'namespace_association',
264 'default' => $nsassociated,
265 'tooltip' => 'namespace_association',
268 'type' => 'tagfilter',
269 'name' => 'tagfilter',
270 'label-raw' => $this->msg( 'tag-filter' )->parse(),
271 'default' => $tagFilterVal,
275 'name' => 'username',
276 'label-message' => 'newpages-username',
277 'default' => $userText,
278 'id' => 'mw-np-username',
282 'type' => 'sizefilter',
284 'default' => -$max * $size,
288 $htmlForm = HTMLForm
::factory( 'ooui', $formDescriptor, $this->getContext() );
290 // Store query values in hidden fields so that form submission doesn't lose them
291 foreach ( $this->opts
->getUnconsumedValues() as $key => $value ) {
292 $htmlForm->addHiddenField( $key, $value );
297 ->setFormIdentifier( 'newpagesform' )
298 // The form should be visible on each request (inclusive requests with submitted forms), so
299 // return always false here.
305 ->setSubmitText( $this->msg( 'newpages-submit' )->text() )
306 ->setWrapperLegend( $this->msg( 'newpages' )->text() )
307 ->addFooterText( Html
::rawElement(
313 $out->addModuleStyles( 'mediawiki.special' );
317 * @param stdClass $result Result row from recent changes
318 * @param Title $title
319 * @return bool|Revision
321 protected function revisionFromRcResult( stdClass
$result, Title
$title ) {
322 return new Revision( [
323 'comment' => CommentStore
::getStore()->getComment( 'rc_comment', $result )->text
,
324 'deleted' => $result->rc_deleted
,
325 'user_text' => $result->rc_user_text
,
326 'user' => $result->rc_user
,
327 'actor' => $result->rc_actor
,
332 * Format a row, providing the timestamp, links to the page/history,
333 * size, user links, and a comment
335 * @param object $result Result row
338 public function formatRow( $result ) {
339 $title = Title
::newFromRow( $result );
341 // Revision deletion works on revisions,
342 // so cast our recent change row to a revision row.
343 $rev = $this->revisionFromRcResult( $result, $title );
346 $attribs = [ 'data-mw-revid' => $result->rev_id
];
348 $lang = $this->getLanguage();
349 $dm = $lang->getDirMark();
351 $spanTime = Html
::element( 'span', [ 'class' => 'mw-newpages-time' ],
352 $lang->userTimeAndDate( $result->rc_timestamp
, $this->getUser() )
354 $linkRenderer = $this->getLinkRenderer();
355 $time = $linkRenderer->makeKnownLink(
357 new HtmlArmor( $spanTime ),
359 [ 'oldid' => $result->rc_this_oldid
]
362 $query = $title->isRedirect() ?
[ 'redirect' => 'no' ] : [];
364 $plink = $linkRenderer->makeKnownLink(
367 [ 'class' => 'mw-newpages-pagename' ],
370 $histLink = $linkRenderer->makeKnownLink(
372 $this->msg( 'hist' )->text(),
374 [ 'action' => 'history' ]
376 $hist = Html
::rawElement( 'span', [ 'class' => 'mw-newpages-history' ],
377 $this->msg( 'parentheses' )->rawParams( $histLink )->escaped() );
379 $length = Html
::rawElement(
381 [ 'class' => 'mw-newpages-length' ],
382 $this->msg( 'brackets' )->rawParams(
383 $this->msg( 'nbytes' )->numParams( $result->length
)->escaped()
387 $ulink = Linker
::revUserTools( $rev );
388 $comment = Linker
::revComment( $rev );
390 if ( $this->patrollable( $result ) ) {
391 $classes[] = 'not-patrolled';
394 # Add a class for zero byte pages
395 if ( $result->length
== 0 ) {
396 $classes[] = 'mw-newpages-zero-byte-page';
400 if ( isset( $result->ts_tags
) ) {
401 list( $tagDisplay, $newClasses ) = ChangeTags
::formatSummaryRow(
406 $classes = array_merge( $classes, $newClasses );
411 # Display the old title if the namespace/title has been changed
413 $oldTitle = Title
::makeTitle( $result->rc_namespace
, $result->rc_title
);
415 if ( !$title->equals( $oldTitle ) ) {
416 $oldTitleText = $oldTitle->getPrefixedText();
417 $oldTitleText = Html
::rawElement(
419 [ 'class' => 'mw-newpages-oldtitle' ],
420 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
424 $ret = "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} "
425 . "{$tagDisplay} {$oldTitleText}";
427 // Let extensions add data
428 Hooks
::run( 'NewPagesLineEnding', [ $this, &$ret, $result, &$classes, &$attribs ] );
429 $attribs = array_filter( $attribs,
430 [ Sanitizer
::class, 'isReservedDataAttribute' ],
434 if ( count( $classes ) ) {
435 $attribs['class'] = implode( ' ', $classes );
438 return Html
::rawElement( 'li', $attribs, $ret ) . "\n";
442 * Should a specific result row provide "patrollable" links?
444 * @param object $result Result row
447 protected function patrollable( $result ) {
448 return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled
);
452 * Output a subscription feed listing recent edits to this page.
454 * @param string $type
456 protected function feed( $type ) {
457 if ( !$this->getConfig()->get( 'Feed' ) ) {
458 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
463 $feedClasses = $this->getConfig()->get( 'FeedClasses' );
464 if ( !isset( $feedClasses[$type] ) ) {
465 $this->getOutput()->addWikiMsg( 'feed-invalid' );
470 $feed = new $feedClasses[$type](
472 $this->msg( 'tagline' )->text(),
473 $this->getPageTitle()->getFullURL()
476 $pager = new NewPagesPager( $this, $this->opts
);
477 $limit = $this->opts
->getValue( 'limit' );
478 $pager->mLimit
= min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
481 if ( $pager->getNumRows() > 0 ) {
482 foreach ( $pager->mResult
as $row ) {
483 $feed->outItem( $this->feedItem( $row ) );
489 protected function feedTitle() {
490 $desc = $this->getDescription();
491 $code = $this->getConfig()->get( 'LanguageCode' );
492 $sitename = $this->getConfig()->get( 'Sitename' );
494 return "$sitename - $desc [$code]";
497 protected function feedItem( $row ) {
498 $title = Title
::makeTitle( intval( $row->rc_namespace
), $row->rc_title
);
500 $date = $row->rc_timestamp
;
501 $comments = $title->getTalkPage()->getFullURL();
504 $title->getPrefixedText(),
505 $this->feedItemDesc( $row ),
506 $title->getFullURL(),
508 $this->feedItemAuthor( $row ),
516 protected function feedItemAuthor( $row ) {
517 return $row->rc_user_text ??
'';
520 protected function feedItemDesc( $row ) {
521 $revision = Revision
::newFromId( $row->rev_id
);
526 $content = $revision->getContent();
527 if ( $content === null ) {
531 // XXX: include content model/type in feed item?
532 return '<p>' . htmlspecialchars( $revision->getUserText() ) .
533 $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
534 htmlspecialchars( FeedItem
::stripComment( $revision->getComment() ) ) .
535 "</p>\n<hr />\n<div>" .
536 nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
539 protected function getGroupName() {
543 protected function getCacheTTL() {