MediaWiki REL1_35
SpecialNewpages.php
Go to the documentation of this file.
1<?php
29
39 protected $opts;
41 protected $customFilters;
42
43 protected $showNavigation = false;
44
45 public function __construct() {
46 parent::__construct( 'Newpages' );
47 }
48
52 protected function setup( $par ) {
53 $opts = new FormOptions();
54 $this->opts = $opts; // bind
55 $opts->add( 'hideliu', false );
56 $opts->add( 'hidepatrolled', $this->getUser()->getBoolOption( 'newpageshidepatrolled' ) );
57 $opts->add( 'hidebots', false );
58 $opts->add( 'hideredirs', true );
59 $opts->add( 'limit', $this->getUser()->getIntOption( 'rclimit' ) );
60 $opts->add( 'offset', '' );
61 $opts->add( 'namespace', '0' );
62 $opts->add( 'username', '' );
63 $opts->add( 'feed', '' );
64 $opts->add( 'tagfilter', '' );
65 $opts->add( 'invert', false );
66 $opts->add( 'associated', false );
67 $opts->add( 'size-mode', 'max' );
68 $opts->add( 'size', 0 );
69
70 $this->customFilters = [];
71 $this->getHookRunner()->onSpecialNewPagesFilters( $this, $this->customFilters );
72 // @phan-suppress-next-line PhanEmptyForeach False positive
73 foreach ( $this->customFilters as $key => $params ) {
74 $opts->add( $key, $params['default'] );
75 }
76
77 $opts->fetchValuesFromRequest( $this->getRequest() );
78 if ( $par ) {
79 $this->parseParams( $par );
80 }
81
82 $opts->validateIntBounds( 'limit', 0, 5000 );
83 }
84
88 protected function parseParams( $par ) {
89 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
90 foreach ( $bits as $bit ) {
91 if ( $bit === 'shownav' ) {
92 $this->showNavigation = true;
93 }
94 if ( $bit === 'hideliu' ) {
95 $this->opts->setValue( 'hideliu', true );
96 }
97 if ( $bit === 'hidepatrolled' ) {
98 $this->opts->setValue( 'hidepatrolled', true );
99 }
100 if ( $bit === 'hidebots' ) {
101 $this->opts->setValue( 'hidebots', true );
102 }
103 if ( $bit === 'showredirs' ) {
104 $this->opts->setValue( 'hideredirs', false );
105 }
106 if ( is_numeric( $bit ) ) {
107 $this->opts->setValue( 'limit', intval( $bit ) );
108 }
109
110 $m = [];
111 if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
112 $this->opts->setValue( 'limit', intval( $m[1] ) );
113 }
114 // PG offsets not just digits!
115 if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
116 $this->opts->setValue( 'offset', intval( $m[1] ) );
117 }
118 if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
119 $this->opts->setValue( 'username', $m[1] );
120 }
121 if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
122 $ns = $this->getLanguage()->getNsIndex( $m[1] );
123 if ( $ns !== false ) {
124 $this->opts->setValue( 'namespace', $ns );
125 }
126 }
127 }
128 }
129
135 public function execute( $par ) {
136 $out = $this->getOutput();
137
138 $this->setHeaders();
139 $this->outputHeader();
140
141 $this->showNavigation = !$this->including(); // Maybe changed in setup
142 $this->setup( $par );
143
144 $this->addHelpLink( 'Help:New pages' );
145
146 if ( !$this->including() ) {
147 // Settings
148 $this->form();
149
150 $feedType = $this->opts->getValue( 'feed' );
151 if ( $feedType ) {
152 $this->feed( $feedType );
153
154 return;
155 }
156
157 $allValues = $this->opts->getAllValues();
158 unset( $allValues['feed'] );
159 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
160 }
161
162 $pager = new NewPagesPager( $this, $this->opts );
163 $pager->mLimit = $this->opts->getValue( 'limit' );
164 $pager->mOffset = $this->opts->getValue( 'offset' );
165
166 if ( $pager->getNumRows() ) {
167 $navigation = '';
168 if ( $this->showNavigation ) {
169 $navigation = $pager->getNavigationBar();
170 }
171 $out->addHTML( $navigation . $pager->getBody() . $navigation );
172 // Add styles for change tags
173 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
174 } else {
175 $out->addWikiMsg( 'specialpage-empty' );
176 }
177 }
178
179 protected function filterLinks() {
180 // show/hide links
181 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
182
183 // Option value -> message mapping
184 $filters = [
185 'hideliu' => 'rcshowhideliu',
186 'hidepatrolled' => 'rcshowhidepatr',
187 'hidebots' => 'rcshowhidebots',
188 'hideredirs' => 'whatlinkshere-hideredirs'
189 ];
190 foreach ( $this->customFilters as $key => $params ) {
191 $filters[$key] = $params['msg'];
192 }
193
194 // Disable some if needed
195 if ( !$this->canAnonymousUsersCreatePages() ) {
196 unset( $filters['hideliu'] );
197 }
198 if ( !$this->getUser()->useNPPatrol() ) {
199 unset( $filters['hidepatrolled'] );
200 }
201
202 $links = [];
203 $changed = $this->opts->getChangedValues();
204 unset( $changed['offset'] ); // Reset offset if query type changes
205
206 // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
207 // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
208 // to 0 or 1.
209 // Also do this only for boolean options, not eg. namespace or tagfilter
210 foreach ( $changed as $key => $value ) {
211 if ( array_key_exists( $key, $filters ) ) {
212 $changed[$key] = $changed[$key] ? '1' : '0';
213 }
214 }
215
216 $self = $this->getPageTitle();
218 foreach ( $filters as $key => $msg ) {
219 $onoff = 1 - $this->opts->getValue( $key );
220 $link = $linkRenderer->makeLink(
221 $self,
222 new HtmlArmor( $showhide[$onoff] ),
223 [],
224 [ $key => $onoff ] + $changed
225 );
226 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
227 }
228
229 return $this->getLanguage()->pipeList( $links );
230 }
231
232 protected function form() {
233 $out = $this->getOutput();
234
235 // Consume values
236 $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
237 $namespace = $this->opts->consumeValue( 'namespace' );
238 $username = $this->opts->consumeValue( 'username' );
239 $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
240 $nsinvert = $this->opts->consumeValue( 'invert' );
241 $nsassociated = $this->opts->consumeValue( 'associated' );
242
243 $size = $this->opts->consumeValue( 'size' );
244 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
245
246 // Check username input validity
247 $ut = Title::makeTitleSafe( NS_USER, $username );
248 $userText = $ut ? $ut->getText() : '';
249
250 $formDescriptor = [
251 'namespace' => [
252 'type' => 'namespaceselect',
253 'name' => 'namespace',
254 'label-message' => 'namespace',
255 'default' => $namespace,
256 ],
257 'nsinvert' => [
258 'type' => 'check',
259 'name' => 'invert',
260 'label-message' => 'invert',
261 'default' => $nsinvert,
262 'tooltip' => 'invert',
263 ],
264 'nsassociated' => [
265 'type' => 'check',
266 'name' => 'associated',
267 'label-message' => 'namespace_association',
268 'default' => $nsassociated,
269 'tooltip' => 'namespace_association',
270 ],
271 'tagFilter' => [
272 'type' => 'tagfilter',
273 'name' => 'tagfilter',
274 'label-raw' => $this->msg( 'tag-filter' )->parse(),
275 'default' => $tagFilterVal,
276 ],
277 'username' => [
278 'type' => 'user',
279 'name' => 'username',
280 'label-message' => 'newpages-username',
281 'default' => $userText,
282 'id' => 'mw-np-username',
283 'size' => 30,
284 ],
285 'size' => [
286 'type' => 'sizefilter',
287 'name' => 'size',
288 'default' => -$max * $size,
289 ],
290 ];
291
292 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
293
294 // Store query values in hidden fields so that form submission doesn't lose them
295 foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
296 $htmlForm->addHiddenField( $key, $value );
297 }
298
299 $htmlForm
300 ->setMethod( 'get' )
301 ->setFormIdentifier( 'newpagesform' )
302 // The form should be visible on each request (inclusive requests with submitted forms), so
303 // return always false here.
304 ->setSubmitCallback(
305 function () {
306 return false;
307 }
308 )
309 ->setSubmitText( $this->msg( 'newpages-submit' )->text() )
310 ->setWrapperLegend( $this->msg( 'newpages' )->text() )
311 ->addFooterText( Html::rawElement(
312 'div',
313 null,
314 $this->filterLinks()
315 ) )
316 ->show();
317 $out->addModuleStyles( 'mediawiki.special' );
318 }
319
325 private function revisionFromRcResult( stdClass $result, Title $title ) : RevisionRecord {
326 $revRecord = new MutableRevisionRecord( $title );
327 $revRecord->setComment(
328 CommentStore::getStore()->getComment( 'rc_comment', $result )
329 );
330 $revRecord->setVisibility( (int)$result->rc_deleted );
331
332 $user = new UserIdentityValue(
333 (int)$result->rc_user,
334 $result->rc_user_text,
335 (int)$result->rc_actor
336 );
337 $revRecord->setUser( $user );
338
339 return $revRecord;
340 }
341
349 public function formatRow( $result ) {
350 $title = Title::newFromRow( $result );
351
352 // Revision deletion works on revisions,
353 // so cast our recent change row to a revision row.
354 $revRecord = $this->revisionFromRcResult( $result, $title );
355
356 $classes = [];
357 $attribs = [ 'data-mw-revid' => $result->rev_id ];
358
359 $lang = $this->getLanguage();
360 $dm = $lang->getDirMark();
361
362 $spanTime = Html::element( 'span', [ 'class' => 'mw-newpages-time' ],
363 $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
364 );
365 $linkRenderer = $this->getLinkRenderer();
366 $time = $linkRenderer->makeKnownLink(
367 $title,
368 new HtmlArmor( $spanTime ),
369 [],
370 [ 'oldid' => $result->rc_this_oldid ]
371 );
372
373 $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
374
375 $plink = $linkRenderer->makeKnownLink(
376 $title,
377 null,
378 [ 'class' => 'mw-newpages-pagename' ],
379 $query
380 );
381 $linkArr = [];
382 $linkArr[] = $linkRenderer->makeKnownLink(
383 $title,
384 $this->msg( 'hist' )->text(),
385 [ 'class' => 'mw-newpages-history' ],
386 [ 'action' => 'history' ]
387 );
388 if ( MediaWikiServices::getInstance()
389 ->getContentHandlerFactory()
390 ->getContentHandler( $title->getContentModel() )
391 ->supportsDirectEditing()
392 ) {
393 $linkArr[] = $linkRenderer->makeKnownLink(
394 $title,
395 $this->msg( 'editlink' )->text(),
396 [ 'class' => 'mw-newpages-edit' ],
397 [ 'action' => 'edit' ]
398 );
399 }
400 $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
401 ->pipeList( $linkArr ) )->escaped();
402
403 $length = Html::rawElement(
404 'span',
405 [ 'class' => 'mw-newpages-length' ],
406 $this->msg( 'brackets' )->rawParams(
407 $this->msg( 'nbytes' )->numParams( $result->length )->escaped()
408 )->escaped()
409 );
410
411 $ulink = Linker::revUserTools( $revRecord );
412 $comment = Linker::revComment( $revRecord );
413
414 if ( $this->patrollable( $result ) ) {
415 $classes[] = 'not-patrolled';
416 }
417
418 # Add a class for zero byte pages
419 if ( $result->length == 0 ) {
420 $classes[] = 'mw-newpages-zero-byte-page';
421 }
422
423 # Tags, if any.
424 if ( isset( $result->ts_tags ) ) {
425 list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
426 $result->ts_tags,
427 'newpages',
428 $this->getContext()
429 );
430 $classes = array_merge( $classes, $newClasses );
431 } else {
432 $tagDisplay = '';
433 }
434
435 # Display the old title if the namespace/title has been changed
436 $oldTitleText = '';
437 $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
438
439 if ( !$title->equals( $oldTitle ) ) {
440 $oldTitleText = $oldTitle->getPrefixedText();
441 $oldTitleText = Html::rawElement(
442 'span',
443 [ 'class' => 'mw-newpages-oldtitle' ],
444 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
445 );
446 }
447
448 $ret = "{$time} {$dm}{$plink} {$links} {$dm}{$length} {$dm}{$ulink} {$comment} "
449 . "{$tagDisplay} {$oldTitleText}";
450
451 // Let extensions add data
452 $this->getHookRunner()->onNewPagesLineEnding(
453 $this, $ret, $result, $classes, $attribs );
454 $attribs = array_filter( $attribs,
455 [ Sanitizer::class, 'isReservedDataAttribute' ],
456 ARRAY_FILTER_USE_KEY
457 );
458
459 if ( count( $classes ) ) {
460 $attribs['class'] = implode( ' ', $classes );
461 }
462
463 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
464 }
465
472 protected function patrollable( $result ) {
473 return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled );
474 }
475
481 protected function feed( $type ) {
482 if ( !$this->getConfig()->get( 'Feed' ) ) {
483 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
484
485 return;
486 }
487
488 $feedClasses = $this->getConfig()->get( 'FeedClasses' );
489 if ( !isset( $feedClasses[$type] ) ) {
490 $this->getOutput()->addWikiMsg( 'feed-invalid' );
491
492 return;
493 }
494
495 $feed = new $feedClasses[$type](
496 $this->feedTitle(),
497 $this->msg( 'tagline' )->text(),
498 $this->getPageTitle()->getFullURL()
499 );
500
501 $pager = new NewPagesPager( $this, $this->opts );
502 $limit = $this->opts->getValue( 'limit' );
503 $pager->mLimit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
504
505 $feed->outHeader();
506 if ( $pager->getNumRows() > 0 ) {
507 foreach ( $pager->mResult as $row ) {
508 $feed->outItem( $this->feedItem( $row ) );
509 }
510 }
511 $feed->outFooter();
512 }
513
514 protected function feedTitle() {
515 $desc = $this->getDescription();
516 $code = $this->getConfig()->get( 'LanguageCode' );
517 $sitename = $this->getConfig()->get( 'Sitename' );
518
519 return "$sitename - $desc [$code]";
520 }
521
522 protected function feedItem( $row ) {
523 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
524 if ( $title ) {
525 $date = $row->rc_timestamp;
526 $comments = $title->getTalkPage()->getFullURL();
527
528 return new FeedItem(
529 $title->getPrefixedText(),
530 $this->feedItemDesc( $row ),
531 $title->getFullURL(),
532 $date,
533 $this->feedItemAuthor( $row ),
534 $comments
535 );
536 } else {
537 return null;
538 }
539 }
540
541 protected function feedItemAuthor( $row ) {
542 return $row->rc_user_text ?? '';
543 }
544
545 protected function feedItemDesc( $row ) {
546 $revisionRecord = MediaWikiServices::getInstance()
547 ->getRevisionLookup()
548 ->getRevisionById( $row->rev_id );
549 if ( !$revisionRecord ) {
550 return '';
551 }
552
553 $content = $revisionRecord->getContent( SlotRecord::MAIN );
554 if ( $content === null ) {
555 return '';
556 }
557
558 // XXX: include content model/type in feed item?
559 $revUser = $revisionRecord->getUser();
560 $revUserText = $revUser ? $revUser->getName() : '';
561 $revComment = $revisionRecord->getComment();
562 $revCommentText = $revComment ? $revComment->text : '';
563 return '<p>' . htmlspecialchars( $revUserText ) .
564 $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
565 htmlspecialchars( FeedItem::stripComment( $revCommentText ) ) .
566 "</p>\n<hr />\n<div>" .
567 nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
568 }
569
570 private function canAnonymousUsersCreatePages() {
571 $pm = MediaWikiServices::getInstance()->getPermissionManager();
572 return ( $pm->groupHasPermission( '*', 'createpage' ) ||
573 $pm->groupHasPermission( '*', 'createtalk' )
574 );
575 }
576
577 protected function getGroupName() {
578 return 'changes';
579 }
580
581 protected function getCacheTTL() {
582 return 60 * 5;
583 }
584}
getUser()
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.
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( $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:1615
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:1152
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Value object representing a content slot associated with a page revision.
Value object representing a user's identity.
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?
getCacheTTL()
Stable to override.
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!...
getOutput()
Get the OutputPage being used for this instance.
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.
MediaWiki Linker LinkRenderer null $linkRenderer
Represents a title within MediaWiki.
Definition Title.php:42
const NS_USER
Definition Defines.php:72
$content
Definition router.php:76
if(!isset( $args[0])) $lang