MediaWiki master
SpecialNewPages.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use HtmlArmor;
41
53 protected $opts;
55 protected $customFilters;
56
57 protected $showNavigation = false;
58
59 private LinkBatchFactory $linkBatchFactory;
60 private IContentHandlerFactory $contentHandlerFactory;
61 private GroupPermissionsLookup $groupPermissionsLookup;
62 private RevisionLookup $revisionLookup;
63 private NamespaceInfo $namespaceInfo;
64 private UserOptionsLookup $userOptionsLookup;
65 private RowCommentFormatter $rowCommentFormatter;
66 private ChangeTagsStore $changeTagsStore;
67
77 public function __construct(
78 LinkBatchFactory $linkBatchFactory,
79 IContentHandlerFactory $contentHandlerFactory,
80 GroupPermissionsLookup $groupPermissionsLookup,
81 RevisionLookup $revisionLookup,
82 NamespaceInfo $namespaceInfo,
83 UserOptionsLookup $userOptionsLookup,
84 RowCommentFormatter $rowCommentFormatter,
85 ChangeTagsStore $changeTagsStore
86 ) {
87 parent::__construct( 'Newpages' );
88 $this->linkBatchFactory = $linkBatchFactory;
89 $this->contentHandlerFactory = $contentHandlerFactory;
90 $this->groupPermissionsLookup = $groupPermissionsLookup;
91 $this->revisionLookup = $revisionLookup;
92 $this->namespaceInfo = $namespaceInfo;
93 $this->userOptionsLookup = $userOptionsLookup;
94 $this->rowCommentFormatter = $rowCommentFormatter;
95 $this->changeTagsStore = $changeTagsStore;
96 }
97
101 protected function setup( $par ) {
102 $opts = new FormOptions();
103 $this->opts = $opts; // bind
104 $opts->add( 'hideliu', false );
105 $opts->add(
106 'hidepatrolled',
107 $this->userOptionsLookup->getBoolOption( $this->getUser(), 'newpageshidepatrolled' )
108 );
109 $opts->add( 'hidebots', false );
110 $opts->add( 'hideredirs', true );
111 $opts->add(
112 'limit',
113 $this->userOptionsLookup->getIntOption( $this->getUser(), 'rclimit' )
114 );
115 $opts->add( 'offset', '' );
116 $opts->add( 'namespace', '0' );
117 $opts->add( 'username', '' );
118 $opts->add( 'feed', '' );
119 $opts->add( 'tagfilter', '' );
120 $opts->add( 'tagInvert', false );
121 $opts->add( 'invert', false );
122 $opts->add( 'associated', false );
123 $opts->add( 'size-mode', 'max' );
124 $opts->add( 'size', 0 );
125
126 $this->customFilters = [];
127 $this->getHookRunner()->onSpecialNewPagesFilters( $this, $this->customFilters );
128 // @phan-suppress-next-line PhanEmptyForeach False positive
129 foreach ( $this->customFilters as $key => $params ) {
130 $opts->add( $key, $params['default'] );
131 }
132
134 if ( $par ) {
135 $this->parseParams( $par );
136 }
137
138 $opts->validateIntBounds( 'limit', 0, 5000 );
139 }
140
144 protected function parseParams( $par ) {
145 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
146 foreach ( $bits as $bit ) {
147 $m = [];
148 if ( $bit === 'shownav' ) {
149 $this->showNavigation = true;
150 } elseif ( $bit === 'hideliu' ) {
151 $this->opts->setValue( 'hideliu', true );
152 } elseif ( $bit === 'hidepatrolled' ) {
153 $this->opts->setValue( 'hidepatrolled', true );
154 } elseif ( $bit === 'hidebots' ) {
155 $this->opts->setValue( 'hidebots', true );
156 } elseif ( $bit === 'showredirs' ) {
157 $this->opts->setValue( 'hideredirs', false );
158 } elseif ( is_numeric( $bit ) ) {
159 $this->opts->setValue( 'limit', intval( $bit ) );
160 } elseif ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
161 $this->opts->setValue( 'limit', intval( $m[1] ) );
162 } elseif ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
163 // PG offsets not just digits!
164 $this->opts->setValue( 'offset', intval( $m[1] ) );
165 } elseif ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
166 $this->opts->setValue( 'username', $m[1] );
167 } elseif ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
168 $ns = $this->getLanguage()->getNsIndex( $m[1] );
169 if ( $ns !== false ) {
170 $this->opts->setValue( 'namespace', $ns );
171 }
172 } else {
173 // T62424 try to interpret unrecognized parameters as a namespace
174 $ns = $this->getLanguage()->getNsIndex( $bit );
175 if ( $ns !== false ) {
176 $this->opts->setValue( 'namespace', $ns );
177 }
178 }
179 }
180 }
181
187 public function execute( $par ) {
188 $out = $this->getOutput();
189
190 $this->setHeaders();
191 $this->outputHeader();
192
193 $this->showNavigation = !$this->including(); // Maybe changed in setup
194 $this->setup( $par );
195
196 $this->addHelpLink( 'Help:New pages' );
197
198 if ( !$this->including() ) {
199 // Settings
200 $this->form();
201
202 $feedType = $this->opts->getValue( 'feed' );
203 if ( $feedType ) {
204 $this->feed( $feedType );
205
206 return;
207 }
208
209 $allValues = $this->opts->getAllValues();
210 unset( $allValues['feed'] );
211 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
212 }
213
214 $pager = $this->getNewPagesPager();
215 $pager->mLimit = $this->opts->getValue( 'limit' );
216 $pager->mOffset = $this->opts->getValue( 'offset' );
217
218 if ( $pager->getNumRows() ) {
219 $navigation = '';
220 if ( $this->showNavigation ) {
221 $navigation = $pager->getNavigationBar();
222 }
223 $out->addHTML( $navigation . $pager->getBody() . $navigation );
224 // Add styles for change tags
225 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
226 } else {
227 $out->addWikiMsg( 'specialpage-empty' );
228 }
229 }
230
231 protected function filterLinks() {
232 // show/hide links
233 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
234
235 // Option value -> message mapping
236 $filters = [
237 'hideliu' => 'newpages-showhide-registered',
238 'hidepatrolled' => 'newpages-showhide-patrolled',
239 'hidebots' => 'newpages-showhide-bots',
240 'hideredirs' => 'newpages-showhide-redirect'
241 ];
242 foreach ( $this->customFilters as $key => $params ) {
243 $filters[$key] = $params['msg'];
244 }
245
246 // Disable some if needed
247 if ( !$this->canAnonymousUsersCreatePages() ) {
248 unset( $filters['hideliu'] );
249 }
250 if ( !$this->getUser()->useNPPatrol() ) {
251 unset( $filters['hidepatrolled'] );
252 }
253
254 $links = [];
255 $changed = $this->opts->getChangedValues();
256 unset( $changed['offset'] ); // Reset offset if query type changes
257
258 // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
259 // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
260 // to 0 or 1.
261 // Also do this only for boolean options, not eg. namespace or tagfilter
262 foreach ( $changed as $key => $value ) {
263 if ( array_key_exists( $key, $filters ) ) {
264 $changed[$key] = $changed[$key] ? '1' : '0';
265 }
266 }
267
268 $self = $this->getPageTitle();
269 $linkRenderer = $this->getLinkRenderer();
270 foreach ( $filters as $key => $msg ) {
271 $onoff = 1 - $this->opts->getValue( $key );
272 $link = $linkRenderer->makeLink(
273 $self,
274 new HtmlArmor( $showhide[$onoff] ),
275 [],
276 [ $key => $onoff ] + $changed
277 );
278 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
279 }
280
281 return $this->getLanguage()->pipeList( $links );
282 }
283
284 protected function form() {
285 $out = $this->getOutput();
286
287 // Consume values
288 $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
289 $namespace = $this->opts->consumeValue( 'namespace' );
290 $username = $this->opts->consumeValue( 'username' );
291 $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
292 $tagInvertVal = $this->opts->consumeValue( 'tagInvert' );
293 $nsinvert = $this->opts->consumeValue( 'invert' );
294 $nsassociated = $this->opts->consumeValue( 'associated' );
295
296 $size = $this->opts->consumeValue( 'size' );
297 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
298
299 // Check username input validity
300 $ut = Title::makeTitleSafe( NS_USER, $username );
301 $userText = $ut ? $ut->getText() : '';
302
303 $formDescriptor = [
304 'namespace' => [
305 'type' => 'namespaceselect',
306 'name' => 'namespace',
307 'label-message' => 'namespace',
308 'default' => $namespace,
309 ],
310 'nsinvert' => [
311 'type' => 'check',
312 'name' => 'invert',
313 'label-message' => 'invert',
314 'default' => $nsinvert,
315 'tooltip' => 'invert',
316 ],
317 'nsassociated' => [
318 'type' => 'check',
319 'name' => 'associated',
320 'label-message' => 'namespace_association',
321 'default' => $nsassociated,
322 'tooltip' => 'namespace_association',
323 ],
324 'tagFilter' => [
325 'type' => 'tagfilter',
326 'name' => 'tagfilter',
327 'label-message' => 'tag-filter',
328 'default' => $tagFilterVal,
329 ],
330 'tagInvert' => [
331 'type' => 'check',
332 'name' => 'tagInvert',
333 'label-message' => 'invert',
334 'hide-if' => [ '===', 'tagFilter', '' ],
335 'default' => $tagInvertVal,
336 ],
337 'username' => [
338 'type' => 'user',
339 'name' => 'username',
340 'label-message' => 'newpages-username',
341 'default' => $userText,
342 'id' => 'mw-np-username',
343 'size' => 30,
344 ],
345 'size' => [
346 'type' => 'sizefilter',
347 'name' => 'size',
348 'default' => ( $max ? -1 : 1 ) * $size,
349 ],
350 ];
351
352 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
353
354 // Store query values in hidden fields so that form submission doesn't lose them
355 foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
356 $htmlForm->addHiddenField( $key, $value );
357 }
358
359 $htmlForm
360 ->setMethod( 'get' )
361 ->setFormIdentifier( 'newpagesform' )
362 // The form should be visible on each request (inclusive requests with submitted forms), so
363 // return always false here.
364 ->setSubmitCallback(
365 static function () {
366 return false;
367 }
368 )
369 ->setSubmitTextMsg( 'newpages-submit' )
370 ->setWrapperLegendMsg( 'newpages' )
371 ->addFooterHtml( Html::rawElement(
372 'div',
373 [],
374 $this->filterLinks()
375 ) )
376 ->show();
377 $out->addModuleStyles( 'mediawiki.special' );
378 }
379
380 private function getNewPagesPager() {
381 return new NewPagesPager(
382 $this->getContext(),
383 $this->getLinkRenderer(),
384 $this->groupPermissionsLookup,
385 $this->getHookContainer(),
386 $this->linkBatchFactory,
387 $this->namespaceInfo,
388 $this->changeTagsStore,
389 $this->rowCommentFormatter,
390 $this->contentHandlerFactory,
391 $this->opts,
392 );
393 }
394
400 protected function feed( $type ) {
401 if ( !$this->getConfig()->get( MainConfigNames::Feed ) ) {
402 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
403
404 return;
405 }
406
407 $feedClasses = $this->getConfig()->get( MainConfigNames::FeedClasses );
408 if ( !isset( $feedClasses[$type] ) ) {
409 $this->getOutput()->addWikiMsg( 'feed-invalid' );
410
411 return;
412 }
413
414 $feed = new $feedClasses[$type](
415 $this->feedTitle(),
416 $this->msg( 'tagline' )->text(),
417 $this->getPageTitle()->getFullURL()
418 );
419
420 $pager = $this->getNewPagesPager();
421 $limit = $this->opts->getValue( 'limit' );
422 $pager->mLimit = min( $limit, $this->getConfig()->get( MainConfigNames::FeedLimit ) );
423
424 $feed->outHeader();
425 if ( $pager->getNumRows() > 0 ) {
426 foreach ( $pager->mResult as $row ) {
427 $feed->outItem( $this->feedItem( $row ) );
428 }
429 }
430 $feed->outFooter();
431 }
432
433 protected function feedTitle() {
434 $desc = $this->getDescription()->text();
435 $code = $this->getConfig()->get( MainConfigNames::LanguageCode );
436 $sitename = $this->getConfig()->get( MainConfigNames::Sitename );
437
438 return "$sitename - $desc [$code]";
439 }
440
441 protected function feedItem( $row ) {
442 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
443 if ( $title ) {
444 $date = $row->rc_timestamp;
445 $comments = $title->getTalkPage()->getFullURL();
446
447 return new FeedItem(
448 $title->getPrefixedText(),
449 $this->feedItemDesc( $row ),
450 $title->getFullURL(),
451 $date,
452 $this->feedItemAuthor( $row ),
453 $comments
454 );
455 } else {
456 return null;
457 }
458 }
459
460 protected function feedItemAuthor( $row ) {
461 return $row->rc_user_text ?? '';
462 }
463
464 protected function feedItemDesc( $row ) {
465 $revisionRecord = $this->revisionLookup->getRevisionById( $row->rev_id );
466 if ( !$revisionRecord ) {
467 return '';
468 }
469
470 $content = $revisionRecord->getContent( SlotRecord::MAIN );
471 if ( $content === null ) {
472 return '';
473 }
474
475 // XXX: include content model/type in feed item?
476 $revUser = $revisionRecord->getUser();
477 $revUserText = $revUser ? $revUser->getName() : '';
478 $revComment = $revisionRecord->getComment();
479 $revCommentText = $revComment ? $revComment->text : '';
480 return '<p>' . htmlspecialchars( $revUserText ) .
481 $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
482 htmlspecialchars( FeedItem::stripComment( $revCommentText ) ) .
483 "</p>\n<hr />\n<div>" .
484 nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
485 }
486
487 private function canAnonymousUsersCreatePages() {
488 return $this->groupPermissionsLookup->groupHasPermission( '*', 'createpage' ) ||
489 $this->groupPermissionsLookup->groupHasPermission( '*', 'createtalk' );
490 }
491
492 protected function getGroupName() {
493 return 'changes';
494 }
495
496 protected function getCacheTTL() {
497 return 60 * 5;
498 }
499}
500
505class_alias( SpecialNewPages::class, 'SpecialNewpages' );
const NS_USER
Definition Defines.php:67
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
array $params
The job parameters.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Gateway class for change_tags table.
This is basically a CommentFormatter with a CommentStore dependency, allowing it to retrieve comment ...
A base class for outputting syndication feeds (e.g.
Definition FeedItem.php:40
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:208
Helper class to keep track of options when mixing links and form elements.
fetchValuesFromRequest(WebRequest $r, $optionKeys=null)
Fetch values for all options (or selected options) from the given WebRequest, making them available f...
add( $name, $default, $type=self::AUTO)
Add an option to be handled by this FormOptions instance.
validateIntBounds( $name, $min, $max)
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
A class containing constants representing the names of configuration variables.
const FeedClasses
Name constant for the FeedClasses setting, for use with Config::get()
const Feed
Name constant for the Feed setting, for use with Config::get()
const Sitename
Name constant for the Sitename setting, for use with Config::get()
const LanguageCode
Name constant for the LanguageCode setting, for use with Config::get()
const FeedLimit
Name constant for the FeedLimit setting, for use with Config::get()
Value object representing a content slot associated with a page revision.
Shortcut to construct an includable special page.
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...
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
including( $x=null)
Whether the special page is being evaluated via transclusion.
getLanguage()
Shortcut to get user's language.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
List of newly created pages.
feed( $type)
Output a subscription feed listing recent edits to this page.
__construct(LinkBatchFactory $linkBatchFactory, IContentHandlerFactory $contentHandlerFactory, GroupPermissionsLookup $groupPermissionsLookup, RevisionLookup $revisionLookup, NamespaceInfo $namespaceInfo, UserOptionsLookup $userOptionsLookup, RowCommentFormatter $rowCommentFormatter, ChangeTagsStore $changeTagsStore)
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
execute( $par)
Show a form for filtering namespace and username.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:79
Provides access to user options.
Service for looking up page revisions.