MediaWiki master
SpecialNewPages.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
23use HtmlArmor;
42
54 protected $opts;
56 protected $customFilters;
57
59 protected $showNavigation = false;
60
61 private LinkBatchFactory $linkBatchFactory;
62 private IContentHandlerFactory $contentHandlerFactory;
63 private GroupPermissionsLookup $groupPermissionsLookup;
64 private RevisionLookup $revisionLookup;
65 private NamespaceInfo $namespaceInfo;
66 private UserOptionsLookup $userOptionsLookup;
67 private RowCommentFormatter $rowCommentFormatter;
68 private ChangeTagsStore $changeTagsStore;
69 private TempUserConfig $tempUserConfig;
70
82 public function __construct(
83 LinkBatchFactory $linkBatchFactory,
84 IContentHandlerFactory $contentHandlerFactory,
85 GroupPermissionsLookup $groupPermissionsLookup,
86 RevisionLookup $revisionLookup,
87 NamespaceInfo $namespaceInfo,
88 UserOptionsLookup $userOptionsLookup,
89 RowCommentFormatter $rowCommentFormatter,
90 ChangeTagsStore $changeTagsStore,
91 TempUserConfig $tempUserConfig
92 ) {
93 parent::__construct( 'Newpages' );
94 $this->linkBatchFactory = $linkBatchFactory;
95 $this->contentHandlerFactory = $contentHandlerFactory;
96 $this->groupPermissionsLookup = $groupPermissionsLookup;
97 $this->revisionLookup = $revisionLookup;
98 $this->namespaceInfo = $namespaceInfo;
99 $this->userOptionsLookup = $userOptionsLookup;
100 $this->rowCommentFormatter = $rowCommentFormatter;
101 $this->changeTagsStore = $changeTagsStore;
102 $this->tempUserConfig = $tempUserConfig;
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( 'tagInvert', false );
128 $opts->add( 'invert', false );
129 $opts->add( 'associated', false );
130 $opts->add( 'size-mode', 'max' );
131 $opts->add( 'size', 0 );
132
133 $this->customFilters = [];
134 $this->getHookRunner()->onSpecialNewPagesFilters( $this, $this->customFilters );
135 // @phan-suppress-next-line PhanEmptyForeach False positive
136 foreach ( $this->customFilters as $key => $params ) {
137 $opts->add( $key, $params['default'] );
138 }
139
141 if ( $par ) {
142 $this->parseParams( $par );
143 }
144
145 // The hideliu option is only available when anonymous users can create pages, as if specified when they
146 // cannot create pages it always would produce no results. Therefore, if anon users cannot create pages
147 // then set hideliu as false overriding the value provided by the user.
148 if ( !$this->canAnonymousUsersCreatePages() ) {
149 $opts->setValue( 'hideliu', false, true );
150 }
151
152 $opts->validateIntBounds( 'limit', 0, 5000 );
153 }
154
158 protected function parseParams( $par ) {
159 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
160 foreach ( $bits as $bit ) {
161 $m = [];
162 if ( $bit === 'shownav' ) {
163 $this->showNavigation = true;
164 } elseif ( $bit === 'hideliu' ) {
165 $this->opts->setValue( 'hideliu', true );
166 } elseif ( $bit === 'hidepatrolled' ) {
167 $this->opts->setValue( 'hidepatrolled', true );
168 } elseif ( $bit === 'hidebots' ) {
169 $this->opts->setValue( 'hidebots', true );
170 } elseif ( $bit === 'showredirs' ) {
171 $this->opts->setValue( 'hideredirs', false );
172 } elseif ( is_numeric( $bit ) ) {
173 $this->opts->setValue( 'limit', intval( $bit ) );
174 } elseif ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
175 $this->opts->setValue( 'limit', intval( $m[1] ) );
176 } elseif ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
177 // PG offsets not just digits!
178 $this->opts->setValue( 'offset', intval( $m[1] ) );
179 } elseif ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
180 $this->opts->setValue( 'username', $m[1] );
181 } elseif ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
182 $ns = $this->getLanguage()->getNsIndex( $m[1] );
183 if ( $ns !== false ) {
184 $this->opts->setValue( 'namespace', $ns );
185 }
186 } else {
187 // T62424 try to interpret unrecognized parameters as a namespace
188 $ns = $this->getLanguage()->getNsIndex( $bit );
189 if ( $ns !== false ) {
190 $this->opts->setValue( 'namespace', $ns );
191 }
192 }
193 }
194 }
195
201 public function execute( $par ) {
202 $out = $this->getOutput();
203
204 $this->setHeaders();
205 $this->outputHeader();
206
207 $this->showNavigation = !$this->including(); // Maybe changed in setup
208 $this->setup( $par );
209
210 $this->addHelpLink( 'Help:New pages' );
211
212 if ( !$this->including() ) {
213 // Settings
214 $this->form();
215
216 $feedType = $this->opts->getValue( 'feed' );
217 if ( $feedType ) {
218 $this->feed( $feedType );
219
220 return;
221 }
222
223 $allValues = $this->opts->getAllValues();
224 unset( $allValues['feed'] );
225 $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
226 }
227
228 $pager = $this->getNewPagesPager();
229 $pager->mLimit = $this->opts->getValue( 'limit' );
230 $pager->mOffset = $this->opts->getValue( 'offset' );
231
232 if ( $pager->getNumRows() ) {
233 $navigation = '';
234 if ( $this->showNavigation ) {
235 $navigation = $pager->getNavigationBar();
236 }
237 $out->addHTML( $navigation . $pager->getBody() . $navigation );
238 // Add styles for change tags
239 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
240 } else {
241 $out->addWikiMsg( 'specialpage-empty' );
242 }
243 }
244
245 protected function filterLinks() {
246 // show/hide links
247 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
248
249 // Option value -> message mapping
250 $filters = [
251 'hideliu' => 'newpages-showhide-registered',
252 'hidepatrolled' => 'newpages-showhide-patrolled',
253 'hidebots' => 'newpages-showhide-bots',
254 'hideredirs' => 'newpages-showhide-redirect'
255 ];
256 foreach ( $this->customFilters as $key => $params ) {
257 $filters[$key] = $params['msg'];
258 }
259
260 // Disable some if needed
261 if ( !$this->canAnonymousUsersCreatePages() ) {
262 unset( $filters['hideliu'] );
263 }
264 if ( !$this->getUser()->useNPPatrol() ) {
265 unset( $filters['hidepatrolled'] );
266 }
267
268 $links = [];
269 $changed = $this->opts->getChangedValues();
270 unset( $changed['offset'] ); // Reset offset if query type changes
271
272 // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
273 // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
274 // to 0 or 1.
275 // Also do this only for boolean options, not eg. namespace or tagfilter
276 foreach ( $changed as $key => $value ) {
277 if ( array_key_exists( $key, $filters ) ) {
278 $changed[$key] = $changed[$key] ? '1' : '0';
279 }
280 }
281
282 $self = $this->getPageTitle();
283 $linkRenderer = $this->getLinkRenderer();
284 foreach ( $filters as $key => $msg ) {
285 $onoff = 1 - $this->opts->getValue( $key );
286 $link = $linkRenderer->makeLink(
287 $self,
288 new HtmlArmor( $showhide[$onoff] ),
289 [],
290 [ $key => $onoff ] + $changed
291 );
292 $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
293 }
294
295 return $this->getLanguage()->pipeList( $links );
296 }
297
298 protected function form() {
299 $out = $this->getOutput();
300
301 // Consume values
302 $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
303 $namespace = $this->opts->consumeValue( 'namespace' );
304 $username = $this->opts->consumeValue( 'username' );
305 $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
306 $tagInvertVal = $this->opts->consumeValue( 'tagInvert' );
307 $nsinvert = $this->opts->consumeValue( 'invert' );
308 $nsassociated = $this->opts->consumeValue( 'associated' );
309
310 $size = $this->opts->consumeValue( 'size' );
311 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
312
313 // Check username input validity
314 $ut = Title::makeTitleSafe( NS_USER, $username );
315 $userText = $ut ? $ut->getText() : '';
316
317 $formDescriptor = [
318 'namespace' => [
319 'type' => 'namespaceselect',
320 'name' => 'namespace',
321 'label-message' => 'namespace',
322 'default' => $namespace,
323 ],
324 'nsinvert' => [
325 'type' => 'check',
326 'name' => 'invert',
327 'label-message' => 'invert',
328 'default' => $nsinvert,
329 'tooltip' => 'invert',
330 ],
331 'nsassociated' => [
332 'type' => 'check',
333 'name' => 'associated',
334 'label-message' => 'namespace_association',
335 'default' => $nsassociated,
336 'tooltip' => 'namespace_association',
337 ],
338 'tagFilter' => [
339 'type' => 'tagfilter',
340 'name' => 'tagfilter',
341 'label-message' => 'tag-filter',
342 'default' => $tagFilterVal,
343 ],
344 'tagInvert' => [
345 'type' => 'check',
346 'name' => 'tagInvert',
347 'label-message' => 'invert',
348 'hide-if' => [ '===', 'tagFilter', '' ],
349 'default' => $tagInvertVal,
350 ],
351 'username' => [
352 'type' => 'user',
353 'name' => 'username',
354 'label-message' => 'newpages-username',
355 'default' => $userText,
356 'id' => 'mw-np-username',
357 'size' => 30,
358 ],
359 'size' => [
360 'type' => 'sizefilter',
361 'name' => 'size',
362 'default' => ( $max ? -1 : 1 ) * $size,
363 ],
364 ];
365
366 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
367
368 // Store query values in hidden fields so that form submission doesn't lose them
369 foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
370 $htmlForm->addHiddenField( $key, $value );
371 }
372
373 $htmlForm
374 ->setMethod( 'get' )
375 ->setFormIdentifier( 'newpagesform' )
376 // The form should be visible on each request (inclusive requests with submitted forms), so
377 // return always false here.
378 ->setSubmitCallback(
379 static function () {
380 return false;
381 }
382 )
383 ->setSubmitTextMsg( 'newpages-submit' )
384 ->setWrapperLegendMsg( 'newpages' )
385 ->addFooterHtml( Html::rawElement(
386 'div',
387 [],
388 $this->filterLinks()
389 ) )
390 ->show();
391 $out->addModuleStyles( 'mediawiki.special' );
392 }
393
394 private function getNewPagesPager() {
395 return new NewPagesPager(
396 $this->getContext(),
397 $this->getLinkRenderer(),
398 $this->groupPermissionsLookup,
399 $this->getHookContainer(),
400 $this->linkBatchFactory,
401 $this->namespaceInfo,
402 $this->changeTagsStore,
403 $this->rowCommentFormatter,
404 $this->contentHandlerFactory,
405 $this->tempUserConfig,
406 $this->opts,
407 );
408 }
409
415 protected function feed( $type ) {
416 if ( !$this->getConfig()->get( MainConfigNames::Feed ) ) {
417 $this->getOutput()->addWikiMsg( 'feed-unavailable' );
418
419 return;
420 }
421
422 $feedClasses = $this->getConfig()->get( MainConfigNames::FeedClasses );
423 if ( !isset( $feedClasses[$type] ) ) {
424 $this->getOutput()->addWikiMsg( 'feed-invalid' );
425
426 return;
427 }
428
429 $feed = new $feedClasses[$type](
430 $this->feedTitle(),
431 $this->msg( 'tagline' )->text(),
432 $this->getPageTitle()->getFullURL()
433 );
434
435 $pager = $this->getNewPagesPager();
436 $limit = $this->opts->getValue( 'limit' );
437 $pager->mLimit = min( $limit, $this->getConfig()->get( MainConfigNames::FeedLimit ) );
438
439 $feed->outHeader();
440 if ( $pager->getNumRows() > 0 ) {
441 foreach ( $pager->mResult as $row ) {
442 $feed->outItem( $this->feedItem( $row ) );
443 }
444 }
445 $feed->outFooter();
446 }
447
448 protected function feedTitle() {
449 $desc = $this->getDescription()->text();
450 $code = $this->getConfig()->get( MainConfigNames::LanguageCode );
451 $sitename = $this->getConfig()->get( MainConfigNames::Sitename );
452
453 return "$sitename - $desc [$code]";
454 }
455
456 protected function feedItem( $row ) {
457 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
458 if ( $title ) {
459 $date = $row->rc_timestamp;
460 $comments = $title->getTalkPage()->getFullURL();
461
462 return new FeedItem(
463 $title->getPrefixedText(),
464 $this->feedItemDesc( $row ),
465 $title->getFullURL(),
466 $date,
467 $this->feedItemAuthor( $row ),
468 $comments
469 );
470 } else {
471 return null;
472 }
473 }
474
475 protected function feedItemAuthor( $row ) {
476 return $row->rc_user_text ?? '';
477 }
478
479 protected function feedItemDesc( $row ) {
480 $revisionRecord = $this->revisionLookup->getRevisionById( $row->rev_id );
481 if ( !$revisionRecord ) {
482 return '';
483 }
484
485 $content = $revisionRecord->getContent( SlotRecord::MAIN );
486 if ( $content === null ) {
487 return '';
488 }
489
490 // XXX: include content model/type in feed item?
491 $revUser = $revisionRecord->getUser();
492 $revUserText = $revUser ? $revUser->getName() : '';
493 $revComment = $revisionRecord->getComment();
494 $revCommentText = $revComment ? $revComment->text : '';
495 return '<p>' . htmlspecialchars( $revUserText ) .
496 $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
497 htmlspecialchars( FeedItem::stripComment( $revCommentText ) ) .
498 "</p>\n<hr />\n<div>" .
499 nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
500 }
501
506 private function canAnonymousUsersCreatePages(): bool {
507 // Get all the groups which anon users can be in.
508 $anonGroups = [ '*' ];
509 if ( $this->tempUserConfig->isKnown() ) {
510 $anonGroups[] = 'temp';
511 }
512 // Check if any of the groups have the createpage or createtalk right.
513 foreach ( $anonGroups as $group ) {
514 $anonUsersCanCreatePages = $this->groupPermissionsLookup->groupHasPermission( $group, 'createpage' ) ||
515 $this->groupPermissionsLookup->groupHasPermission( $group, 'createtalk' );
516 if ( $anonUsersCanCreatePages ) {
517 return true;
518 }
519 }
520 return false;
521 }
522
523 protected function getGroupName() {
524 return 'changes';
525 }
526
527 protected function getCacheTTL() {
528 return 60 * 5;
529 }
530}
531
536class_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
Read-write access to the 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:209
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.
setValue( $name, $value, $force=false)
Use to set the value of an option.
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, TempUserConfig $tempUserConfig)
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:78
Provides access to user options.
Service for looking up page revisions.
Interface for temporary user creation config and name matching.