MediaWiki REL1_34
SpecialNewpages.php
Go to the documentation of this file.
1<?php
25
35 protected $opts;
36 protected $customFilters;
37
38 protected $showNavigation = false;
39
40 public function __construct() {
41 parent::__construct( 'Newpages' );
42 }
43
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 );
64
65 $this->customFilters = [];
66 Hooks::run( 'SpecialNewPagesFilters', [ $this, &$this->customFilters ] );
67 foreach ( $this->customFilters as $key => $params ) {
68 $opts->add( $key, $params['default'] );
69 }
70
72 if ( $par ) {
73 $this->parseParams( $par );
74 }
75
76 $opts->validateIntBounds( 'limit', 0, 5000 );
77 }
78
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;
87 }
88 if ( $bit === 'hideliu' ) {
89 $this->opts->setValue( 'hideliu', true );
90 }
91 if ( $bit === 'hidepatrolled' ) {
92 $this->opts->setValue( 'hidepatrolled', true );
93 }
94 if ( $bit === 'hidebots' ) {
95 $this->opts->setValue( 'hidebots', true );
96 }
97 if ( $bit === 'showredirs' ) {
98 $this->opts->setValue( 'hideredirs', false );
99 }
100 if ( is_numeric( $bit ) ) {
101 $this->opts->setValue( 'limit', intval( $bit ) );
102 }
103
104 $m = [];
105 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
106 $this->opts->setValue( 'limit', intval( $m[1] ) );
107 }
108 // PG offsets not just digits!
109 if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
110 $this->opts->setValue( 'offset', intval( $m[1] ) );
111 }
112 if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
113 $this->opts->setValue( 'username', $m[1] );
114 }
115 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
116 $ns = $this->getLanguage()->getNsIndex( $m[1] );
117 if ( $ns !== false ) {
118 $this->opts->setValue( 'namespace', $ns );
119 }
120 }
121 }
122 }
123
129 public function execute( $par ) {
130 $out = $this->getOutput();
131
132 $this->setHeaders();
133 $this->outputHeader();
134
135 $this->showNavigation = !$this->including(); // Maybe changed in setup
136 $this->setup( $par );
137
138 $this->addHelpLink( 'Help:New pages' );
139
140 if ( !$this->including() ) {
141 // Settings
142 $this->form();
143
144 $feedType = $this->opts->getValue( 'feed' );
145 if ( $feedType ) {
146 $this->feed( $feedType );
147
148 return;
149 }
150
151 $allValues = $this->opts->getAllValues();
152 unset( $allValues['feed'] );
153 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
154 }
155
156 $pager = new NewPagesPager( $this, $this->opts );
157 $pager->mLimit = $this->opts->getValue( 'limit' );
158 $pager->mOffset = $this->opts->getValue( 'offset' );
159
160 if ( $pager->getNumRows() ) {
161 $navigation = '';
162 if ( $this->showNavigation ) {
163 $navigation = $pager->getNavigationBar();
164 }
165 $out->addHTML( $navigation . $pager->getBody() . $navigation );
166 // Add styles for change tags
167 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
168 } else {
169 $out->addWikiMsg( 'specialpage-empty' );
170 }
171 }
172
173 protected function filterLinks() {
174 // show/hide links
175 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
176
177 // Option value -> message mapping
178 $filters = [
179 'hideliu' => 'rcshowhideliu',
180 'hidepatrolled' => 'rcshowhidepatr',
181 'hidebots' => 'rcshowhidebots',
182 'hideredirs' => 'whatlinkshere-hideredirs'
183 ];
184 foreach ( $this->customFilters as $key => $params ) {
185 $filters[$key] = $params['msg'];
186 }
187
188 // Disable some if needed
189 if ( !MediaWikiServices::getInstance()->getPermissionManager()
190 ->groupHasPermission( '*', 'createpage' )
191 ) {
192 unset( $filters['hideliu'] );
193 }
194 if ( !$this->getUser()->useNPPatrol() ) {
195 unset( $filters['hidepatrolled'] );
196 }
197
198 $links = [];
199 $changed = $this->opts->getChangedValues();
200 unset( $changed['offset'] ); // Reset offset if query type changes
201
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
204 // to 0 or 1.
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';
209 }
210 }
211
212 $self = $this->getPageTitle();
214 foreach ( $filters as $key => $msg ) {
215 $onoff = 1 - $this->opts->getValue( $key );
216 $link = $linkRenderer->makeLink(
217 $self,
218 new HtmlArmor( $showhide[$onoff] ),
219 [],
220 [ $key => $onoff ] + $changed
221 );
222 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
223 }
224
225 return $this->getLanguage()->pipeList( $links );
226 }
227
228 protected function form() {
229 $out = $this->getOutput();
230
231 // Consume values
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' );
238
239 $size = $this->opts->consumeValue( 'size' );
240 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
241
242 // Check username input validity
243 $ut = Title::makeTitleSafe( NS_USER, $username );
244 $userText = $ut ? $ut->getText() : '';
245
246 $formDescriptor = [
247 'namespace' => [
248 'type' => 'namespaceselect',
249 'name' => 'namespace',
250 'label-message' => 'namespace',
251 'default' => $namespace,
252 ],
253 'nsinvert' => [
254 'type' => 'check',
255 'name' => 'invert',
256 'label-message' => 'invert',
257 'default' => $nsinvert,
258 'tooltip' => 'invert',
259 ],
260 'nsassociated' => [
261 'type' => 'check',
262 'name' => 'associated',
263 'label-message' => 'namespace_association',
264 'default' => $nsassociated,
265 'tooltip' => 'namespace_association',
266 ],
267 'tagFilter' => [
268 'type' => 'tagfilter',
269 'name' => 'tagfilter',
270 'label-raw' => $this->msg( 'tag-filter' )->parse(),
271 'default' => $tagFilterVal,
272 ],
273 'username' => [
274 'type' => 'user',
275 'name' => 'username',
276 'label-message' => 'newpages-username',
277 'default' => $userText,
278 'id' => 'mw-np-username',
279 'size' => 30,
280 ],
281 'size' => [
282 'type' => 'sizefilter',
283 'name' => 'size',
284 'default' => -$max * $size,
285 ],
286 ];
287
288 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
289
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 );
293 }
294
295 $htmlForm
296 ->setMethod( 'get' )
297 ->setFormIdentifier( 'newpagesform' )
298 // The form should be visible on each request (inclusive requests with submitted forms), so
299 // return always false here.
300 ->setSubmitCallback(
301 function () {
302 return false;
303 }
304 )
305 ->setSubmitText( $this->msg( 'newpages-submit' )->text() )
306 ->setWrapperLegend( $this->msg( 'newpages' )->text() )
307 ->addFooterText( Html::rawElement(
308 'div',
309 null,
310 $this->filterLinks()
311 ) )
312 ->show();
313 $out->addModuleStyles( 'mediawiki.special' );
314 }
315
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,
328 ], 0, $title );
329 }
330
338 public function formatRow( $result ) {
339 $title = Title::newFromRow( $result );
340
341 // Revision deletion works on revisions,
342 // so cast our recent change row to a revision row.
343 $rev = $this->revisionFromRcResult( $result, $title );
344
345 $classes = [];
346 $attribs = [ 'data-mw-revid' => $result->rev_id ];
347
348 $lang = $this->getLanguage();
349 $dm = $lang->getDirMark();
350
351 $spanTime = Html::element( 'span', [ 'class' => 'mw-newpages-time' ],
352 $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
353 );
355 $time = $linkRenderer->makeKnownLink(
356 $title,
357 new HtmlArmor( $spanTime ),
358 [],
359 [ 'oldid' => $result->rc_this_oldid ]
360 );
361
362 $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
363
364 $plink = $linkRenderer->makeKnownLink(
365 $title,
366 null,
367 [ 'class' => 'mw-newpages-pagename' ],
368 $query
369 );
370 $histLink = $linkRenderer->makeKnownLink(
371 $title,
372 $this->msg( 'hist' )->text(),
373 [],
374 [ 'action' => 'history' ]
375 );
376 $hist = Html::rawElement( 'span', [ 'class' => 'mw-newpages-history' ],
377 $this->msg( 'parentheses' )->rawParams( $histLink )->escaped() );
378
379 $length = Html::rawElement(
380 'span',
381 [ 'class' => 'mw-newpages-length' ],
382 $this->msg( 'brackets' )->rawParams(
383 $this->msg( 'nbytes' )->numParams( $result->length )->escaped()
384 )->escaped()
385 );
386
387 $ulink = Linker::revUserTools( $rev );
388 $comment = Linker::revComment( $rev );
389
390 if ( $this->patrollable( $result ) ) {
391 $classes[] = 'not-patrolled';
392 }
393
394 # Add a class for zero byte pages
395 if ( $result->length == 0 ) {
396 $classes[] = 'mw-newpages-zero-byte-page';
397 }
398
399 # Tags, if any.
400 if ( isset( $result->ts_tags ) ) {
401 list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
402 $result->ts_tags,
403 'newpages',
404 $this->getContext()
405 );
406 $classes = array_merge( $classes, $newClasses );
407 } else {
408 $tagDisplay = '';
409 }
410
411 # Display the old title if the namespace/title has been changed
412 $oldTitleText = '';
413 $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
414
415 if ( !$title->equals( $oldTitle ) ) {
416 $oldTitleText = $oldTitle->getPrefixedText();
417 $oldTitleText = Html::rawElement(
418 'span',
419 [ 'class' => 'mw-newpages-oldtitle' ],
420 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
421 );
422 }
423
424 $ret = "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} "
425 . "{$tagDisplay} {$oldTitleText}";
426
427 // Let extensions add data
428 Hooks::run( 'NewPagesLineEnding', [ $this, &$ret, $result, &$classes, &$attribs ] );
429 $attribs = array_filter( $attribs,
430 [ Sanitizer::class, 'isReservedDataAttribute' ],
431 ARRAY_FILTER_USE_KEY
432 );
433
434 if ( count( $classes ) ) {
435 $attribs['class'] = implode( ' ', $classes );
436 }
437
438 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
439 }
440
447 protected function patrollable( $result ) {
448 return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled );
449 }
450
456 protected function feed( $type ) {
457 if ( !$this->getConfig()->get( 'Feed' ) ) {
458 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
459
460 return;
461 }
462
463 $feedClasses = $this->getConfig()->get( 'FeedClasses' );
464 if ( !isset( $feedClasses[$type] ) ) {
465 $this->getOutput()->addWikiMsg( 'feed-invalid' );
466
467 return;
468 }
469
470 $feed = new $feedClasses[$type](
471 $this->feedTitle(),
472 $this->msg( 'tagline' )->text(),
473 $this->getPageTitle()->getFullURL()
474 );
475
476 $pager = new NewPagesPager( $this, $this->opts );
477 $limit = $this->opts->getValue( 'limit' );
478 $pager->mLimit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
479
480 $feed->outHeader();
481 if ( $pager->getNumRows() > 0 ) {
482 foreach ( $pager->mResult as $row ) {
483 $feed->outItem( $this->feedItem( $row ) );
484 }
485 }
486 $feed->outFooter();
487 }
488
489 protected function feedTitle() {
490 $desc = $this->getDescription();
491 $code = $this->getConfig()->get( 'LanguageCode' );
492 $sitename = $this->getConfig()->get( 'Sitename' );
493
494 return "$sitename - $desc [$code]";
495 }
496
497 protected function feedItem( $row ) {
498 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
499 if ( $title ) {
500 $date = $row->rc_timestamp;
501 $comments = $title->getTalkPage()->getFullURL();
502
503 return new FeedItem(
504 $title->getPrefixedText(),
505 $this->feedItemDesc( $row ),
506 $title->getFullURL(),
507 $date,
508 $this->feedItemAuthor( $row ),
509 $comments
510 );
511 } else {
512 return null;
513 }
514 }
515
516 protected function feedItemAuthor( $row ) {
517 return $row->rc_user_text ?? '';
518 }
519
520 protected function feedItemDesc( $row ) {
521 $revision = Revision::newFromId( $row->rev_id );
522 if ( !$revision ) {
523 return '';
524 }
525
526 $content = $revision->getContent();
527 if ( $content === null ) {
528 return '';
529 }
530
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>";
537 }
538
539 protected function getGroupName() {
540 return 'changes';
541 }
542
543 protected function getCacheTTL() {
544 return 60 * 5;
545 }
546}
getPermissionManager()
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:33
static stripComment( $text)
Quickie hack... strip out wikilinks to more legible form from the comment.
Definition FeedItem.php:218
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:28
Shortcut to construct an includable special page.
static revComment(Revision $rev, $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:1577
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1124
MediaWikiServices is the service locator for the application scope of MediaWiki.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:119
A special page that list newly created pages.
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?
feed( $type)
Output a subscription feed listing recent edits to this page.
execute( $par)
Show a form for filtering namespace and username.
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!
getDescription()
Returns the name that goes in the <h1> in the special page itself, and also the name that will be l...
getOutput()
Get the OutputPage being used for this instance.
getUser()
Shortcut to get the User executing this instance.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
Represents a title within MediaWiki.
Definition Title.php:42
const NS_USER
Definition Defines.php:71
$content
Definition router.php:78
if(!isset( $args[0])) $lang