MediaWiki  master
SpecialNewpages.php
Go to the documentation of this file.
1 <?php
42 
52  protected $opts;
54  protected $customFilters;
55 
56  protected $showNavigation = false;
57 
59  private $linkBatchFactory;
60 
62  private $commentStore;
63 
65  private $contentHandlerFactory;
66 
68  private $groupPermissionsLookup;
69 
71  private $loadBalancer;
72 
74  private $revisionLookup;
75 
77  private $namespaceInfo;
78 
80  private $userOptionsLookup;
81 
83  private $commentFormatter;
84 
96  public function __construct(
97  LinkBatchFactory $linkBatchFactory,
98  CommentStore $commentStore,
99  IContentHandlerFactory $contentHandlerFactory,
100  GroupPermissionsLookup $groupPermissionsLookup,
101  ILoadBalancer $loadBalancer,
102  RevisionLookup $revisionLookup,
103  NamespaceInfo $namespaceInfo,
104  UserOptionsLookup $userOptionsLookup,
105  CommentFormatter $commentFormatter
106  ) {
107  parent::__construct( 'Newpages' );
108  $this->linkBatchFactory = $linkBatchFactory;
109  $this->commentStore = $commentStore;
110  $this->contentHandlerFactory = $contentHandlerFactory;
111  $this->groupPermissionsLookup = $groupPermissionsLookup;
112  $this->loadBalancer = $loadBalancer;
113  $this->revisionLookup = $revisionLookup;
114  $this->namespaceInfo = $namespaceInfo;
115  $this->userOptionsLookup = $userOptionsLookup;
116  $this->commentFormatter = $commentFormatter;
117  }
118 
122  protected function setup( $par ) {
123  $opts = new FormOptions();
124  $this->opts = $opts; // bind
125  $opts->add( 'hideliu', false );
126  $opts->add(
127  'hidepatrolled',
128  $this->userOptionsLookup->getBoolOption( $this->getUser(), 'newpageshidepatrolled' )
129  );
130  $opts->add( 'hidebots', false );
131  $opts->add( 'hideredirs', true );
132  $opts->add(
133  'limit',
134  $this->userOptionsLookup->getIntOption( $this->getUser(), 'rclimit' )
135  );
136  $opts->add( 'offset', '' );
137  $opts->add( 'namespace', '0' );
138  $opts->add( 'username', '' );
139  $opts->add( 'feed', '' );
140  $opts->add( 'tagfilter', '' );
141  $opts->add( 'invert', false );
142  $opts->add( 'associated', false );
143  $opts->add( 'size-mode', 'max' );
144  $opts->add( 'size', 0 );
145 
146  $this->customFilters = [];
147  $this->getHookRunner()->onSpecialNewPagesFilters( $this, $this->customFilters );
148  // @phan-suppress-next-line PhanEmptyForeach False positive
149  foreach ( $this->customFilters as $key => $params ) {
150  $opts->add( $key, $params['default'] );
151  }
152 
154  if ( $par ) {
155  $this->parseParams( $par );
156  }
157 
158  $opts->validateIntBounds( 'limit', 0, 5000 );
159  }
160 
164  protected function parseParams( $par ) {
165  $bits = preg_split( '/\s*,\s*/', trim( $par ) );
166  foreach ( $bits as $bit ) {
167  if ( $bit === 'shownav' ) {
168  $this->showNavigation = true;
169  }
170  if ( $bit === 'hideliu' ) {
171  $this->opts->setValue( 'hideliu', true );
172  }
173  if ( $bit === 'hidepatrolled' ) {
174  $this->opts->setValue( 'hidepatrolled', true );
175  }
176  if ( $bit === 'hidebots' ) {
177  $this->opts->setValue( 'hidebots', true );
178  }
179  if ( $bit === 'showredirs' ) {
180  $this->opts->setValue( 'hideredirs', false );
181  }
182  if ( is_numeric( $bit ) ) {
183  $this->opts->setValue( 'limit', intval( $bit ) );
184  }
185 
186  $m = [];
187  if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
188  $this->opts->setValue( 'limit', intval( $m[1] ) );
189  }
190  // PG offsets not just digits!
191  if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
192  $this->opts->setValue( 'offset', intval( $m[1] ) );
193  }
194  if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
195  $this->opts->setValue( 'username', $m[1] );
196  }
197  if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
198  $ns = $this->getLanguage()->getNsIndex( $m[1] );
199  if ( $ns !== false ) {
200  $this->opts->setValue( 'namespace', $ns );
201  }
202  }
203  }
204  }
205 
211  public function execute( $par ) {
212  $out = $this->getOutput();
213 
214  $this->setHeaders();
215  $this->outputHeader();
216 
217  $this->showNavigation = !$this->including(); // Maybe changed in setup
218  $this->setup( $par );
219 
220  $this->addHelpLink( 'Help:New pages' );
221 
222  if ( !$this->including() ) {
223  // Settings
224  $this->form();
225 
226  $feedType = $this->opts->getValue( 'feed' );
227  if ( $feedType ) {
228  $this->feed( $feedType );
229 
230  return;
231  }
232 
233  $allValues = $this->opts->getAllValues();
234  unset( $allValues['feed'] );
235  $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
236  }
237 
238  $pager = $this->getNewPagesPager();
239  $pager->mLimit = $this->opts->getValue( 'limit' );
240  $pager->mOffset = $this->opts->getValue( 'offset' );
241 
242  if ( $pager->getNumRows() ) {
243  $navigation = '';
244  if ( $this->showNavigation ) {
245  $navigation = $pager->getNavigationBar();
246  }
247  $out->addHTML( $navigation . $pager->getBody() . $navigation );
248  // Add styles for change tags
249  $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
250  } else {
251  $out->addWikiMsg( 'specialpage-empty' );
252  }
253  }
254 
255  protected function filterLinks() {
256  // show/hide links
257  $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
258 
259  // Option value -> message mapping
260  $filters = [
261  'hideliu' => 'newpages-showhide-registered',
262  'hidepatrolled' => 'newpages-showhide-patrolled',
263  'hidebots' => 'newpages-showhide-bots',
264  'hideredirs' => 'newpages-showhide-redirect'
265  ];
266  foreach ( $this->customFilters as $key => $params ) {
267  $filters[$key] = $params['msg'];
268  }
269 
270  // Disable some if needed
271  if ( !$this->canAnonymousUsersCreatePages() ) {
272  unset( $filters['hideliu'] );
273  }
274  if ( !$this->getUser()->useNPPatrol() ) {
275  unset( $filters['hidepatrolled'] );
276  }
277 
278  $links = [];
279  $changed = $this->opts->getChangedValues();
280  unset( $changed['offset'] ); // Reset offset if query type changes
281 
282  // wfArrayToCgi(), called from LinkRenderer/Title, will not output null and false values
283  // to the URL, which would omit some options (T158504). Fix it by explicitly setting them
284  // to 0 or 1.
285  // Also do this only for boolean options, not eg. namespace or tagfilter
286  foreach ( $changed as $key => $value ) {
287  if ( array_key_exists( $key, $filters ) ) {
288  $changed[$key] = $changed[$key] ? '1' : '0';
289  }
290  }
291 
292  $self = $this->getPageTitle();
293  $linkRenderer = $this->getLinkRenderer();
294  foreach ( $filters as $key => $msg ) {
295  $onoff = 1 - $this->opts->getValue( $key );
296  $link = $linkRenderer->makeLink(
297  $self,
298  new HtmlArmor( $showhide[$onoff] ),
299  [],
300  [ $key => $onoff ] + $changed
301  );
302  $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
303  }
304 
305  return $this->getLanguage()->pipeList( $links );
306  }
307 
308  protected function form() {
309  $out = $this->getOutput();
310 
311  // Consume values
312  $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
313  $namespace = $this->opts->consumeValue( 'namespace' );
314  $username = $this->opts->consumeValue( 'username' );
315  $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
316  $nsinvert = $this->opts->consumeValue( 'invert' );
317  $nsassociated = $this->opts->consumeValue( 'associated' );
318 
319  $size = $this->opts->consumeValue( 'size' );
320  $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
321 
322  // Check username input validity
323  $ut = Title::makeTitleSafe( NS_USER, $username );
324  $userText = $ut ? $ut->getText() : '';
325 
326  $formDescriptor = [
327  'namespace' => [
328  'type' => 'namespaceselect',
329  'name' => 'namespace',
330  'label-message' => 'namespace',
331  'default' => $namespace,
332  ],
333  'nsinvert' => [
334  'type' => 'check',
335  'name' => 'invert',
336  'label-message' => 'invert',
337  'default' => $nsinvert,
338  'tooltip' => 'invert',
339  ],
340  'nsassociated' => [
341  'type' => 'check',
342  'name' => 'associated',
343  'label-message' => 'namespace_association',
344  'default' => $nsassociated,
345  'tooltip' => 'namespace_association',
346  ],
347  'tagFilter' => [
348  'type' => 'tagfilter',
349  'name' => 'tagfilter',
350  'label-message' => 'tag-filter',
351  'default' => $tagFilterVal,
352  ],
353  'username' => [
354  'type' => 'user',
355  'name' => 'username',
356  'label-message' => 'newpages-username',
357  'default' => $userText,
358  'id' => 'mw-np-username',
359  'size' => 30,
360  ],
361  'size' => [
362  'type' => 'sizefilter',
363  'name' => 'size',
364  'default' => ( $max ? -1 : 1 ) * $size,
365  ],
366  ];
367 
368  $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
369 
370  // Store query values in hidden fields so that form submission doesn't lose them
371  foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
372  $htmlForm->addHiddenField( $key, $value );
373  }
374 
375  $htmlForm
376  ->setMethod( 'get' )
377  ->setFormIdentifier( 'newpagesform' )
378  // The form should be visible on each request (inclusive requests with submitted forms), so
379  // return always false here.
380  ->setSubmitCallback(
381  static function () {
382  return false;
383  }
384  )
385  ->setSubmitTextMsg( 'newpages-submit' )
386  ->setWrapperLegendMsg( 'newpages' )
387  ->addFooterHtml( Html::rawElement(
388  'div',
389  [],
390  $this->filterLinks()
391  ) )
392  ->show();
393  $out->addModuleStyles( 'mediawiki.special' );
394  }
395 
401  protected function revisionFromRcResult( stdClass $result, Title $title ): RevisionRecord {
402  $revRecord = new MutableRevisionRecord( $title );
403  $revRecord->setComment(
404  $this->commentStore->getComment( 'rc_comment', $result )
405  );
406  $revRecord->setVisibility( (int)$result->rc_deleted );
407 
408  $user = new UserIdentityValue(
409  (int)$result->rc_user,
410  $result->rc_user_text
411  );
412  $revRecord->setUser( $user );
413 
414  return $revRecord;
415  }
416 
424  public function formatRow( $result ) {
425  $title = Title::newFromRow( $result );
426 
427  // Revision deletion works on revisions,
428  // so cast our recent change row to a revision row.
429  $revRecord = $this->revisionFromRcResult( $result, $title );
430 
431  $classes = [];
432  $attribs = [ 'data-mw-revid' => $result->rev_id ];
433 
434  $lang = $this->getLanguage();
435  $dm = $lang->getDirMark();
436 
437  $spanTime = Html::element( 'span', [ 'class' => 'mw-newpages-time' ],
438  $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
439  );
440  $linkRenderer = $this->getLinkRenderer();
441  $time = $linkRenderer->makeKnownLink(
442  $title,
443  new HtmlArmor( $spanTime ),
444  [],
445  [ 'oldid' => $result->rc_this_oldid ]
446  );
447 
448  $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
449 
450  $plink = $linkRenderer->makeKnownLink(
451  $title,
452  null,
453  [ 'class' => 'mw-newpages-pagename' ],
454  $query
455  );
456  $linkArr = [];
457  $linkArr[] = $linkRenderer->makeKnownLink(
458  $title,
459  $this->msg( 'hist' )->text(),
460  [ 'class' => 'mw-newpages-history' ],
461  [ 'action' => 'history' ]
462  );
463  if ( $this->contentHandlerFactory->getContentHandler( $title->getContentModel() )
464  ->supportsDirectEditing()
465  ) {
466  $linkArr[] = $linkRenderer->makeKnownLink(
467  $title,
468  $this->msg( 'editlink' )->text(),
469  [ 'class' => 'mw-newpages-edit' ],
470  [ 'action' => 'edit' ]
471  );
472  }
473  $links = $this->msg( 'parentheses' )->rawParams( $this->getLanguage()
474  ->pipeList( $linkArr ) )->escaped();
475 
476  $length = Html::rawElement(
477  'span',
478  [ 'class' => 'mw-newpages-length' ],
479  $this->msg( 'brackets' )->rawParams(
480  $this->msg( 'nbytes' )->numParams( $result->length )->escaped()
481  )->escaped()
482  );
483 
484  $ulink = Linker::revUserTools( $revRecord );
485  $comment = $this->commentFormatter->formatRevision( $revRecord, $this->getAuthority() );
486 
487  if ( $this->patrollable( $result ) ) {
488  $classes[] = 'not-patrolled';
489  }
490 
491  # Add a class for zero byte pages
492  if ( $result->length == 0 ) {
493  $classes[] = 'mw-newpages-zero-byte-page';
494  }
495 
496  # Tags, if any.
497  if ( isset( $result->ts_tags ) ) {
498  [ $tagDisplay, $newClasses ] = ChangeTags::formatSummaryRow(
499  $result->ts_tags,
500  'newpages',
501  $this->getContext()
502  );
503  $classes = array_merge( $classes, $newClasses );
504  } else {
505  $tagDisplay = '';
506  }
507 
508  # Display the old title if the namespace/title has been changed
509  $oldTitleText = '';
510  $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
511 
512  if ( !$title->equals( $oldTitle ) ) {
513  $oldTitleText = $oldTitle->getPrefixedText();
514  $oldTitleText = Html::rawElement(
515  'span',
516  [ 'class' => 'mw-newpages-oldtitle' ],
517  $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
518  );
519  }
520 
521  $ret = "{$time} {$dm}{$plink} {$links} {$dm}{$length} {$dm}{$ulink} {$comment} "
522  . "{$tagDisplay} {$oldTitleText}";
523 
524  // Let extensions add data
525  $this->getHookRunner()->onNewPagesLineEnding(
526  $this, $ret, $result, $classes, $attribs );
527  $attribs = array_filter( $attribs,
528  [ Sanitizer::class, 'isReservedDataAttribute' ],
529  ARRAY_FILTER_USE_KEY
530  );
531 
532  if ( $classes ) {
533  $attribs['class'] = $classes;
534  }
535 
536  return Html::rawElement( 'li', $attribs, $ret ) . "\n";
537  }
538 
539  private function getNewPagesPager() {
540  return new NewPagesPager(
541  $this,
542  $this->groupPermissionsLookup,
543  $this->getHookContainer(),
544  $this->linkBatchFactory,
545  $this->loadBalancer,
546  $this->namespaceInfo,
547  $this->opts
548  );
549  }
550 
557  protected function patrollable( $result ) {
558  return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled );
559  }
560 
566  protected function feed( $type ) {
567  if ( !$this->getConfig()->get( MainConfigNames::Feed ) ) {
568  $this->getOutput()->addWikiMsg( 'feed-unavailable' );
569 
570  return;
571  }
572 
573  $feedClasses = $this->getConfig()->get( MainConfigNames::FeedClasses );
574  if ( !isset( $feedClasses[$type] ) ) {
575  $this->getOutput()->addWikiMsg( 'feed-invalid' );
576 
577  return;
578  }
579 
580  $feed = new $feedClasses[$type](
581  $this->feedTitle(),
582  $this->msg( 'tagline' )->text(),
583  $this->getPageTitle()->getFullURL()
584  );
585 
586  $pager = $this->getNewPagesPager();
587  $limit = $this->opts->getValue( 'limit' );
588  $pager->mLimit = min( $limit, $this->getConfig()->get( MainConfigNames::FeedLimit ) );
589 
590  $feed->outHeader();
591  if ( $pager->getNumRows() > 0 ) {
592  foreach ( $pager->mResult as $row ) {
593  $feed->outItem( $this->feedItem( $row ) );
594  }
595  }
596  $feed->outFooter();
597  }
598 
599  protected function feedTitle() {
600  $desc = $this->getDescription();
601  $code = $this->getConfig()->get( MainConfigNames::LanguageCode );
602  $sitename = $this->getConfig()->get( MainConfigNames::Sitename );
603 
604  return "$sitename - $desc [$code]";
605  }
606 
607  protected function feedItem( $row ) {
608  $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
609  if ( $title ) {
610  $date = $row->rc_timestamp;
611  $comments = $title->getTalkPage()->getFullURL();
612 
613  return new FeedItem(
614  $title->getPrefixedText(),
615  $this->feedItemDesc( $row ),
616  $title->getFullURL(),
617  $date,
618  $this->feedItemAuthor( $row ),
619  $comments
620  );
621  } else {
622  return null;
623  }
624  }
625 
626  protected function feedItemAuthor( $row ) {
627  return $row->rc_user_text ?? '';
628  }
629 
630  protected function feedItemDesc( $row ) {
631  $revisionRecord = $this->revisionLookup->getRevisionById( $row->rev_id );
632  if ( !$revisionRecord ) {
633  return '';
634  }
635 
636  $content = $revisionRecord->getContent( SlotRecord::MAIN );
637  if ( $content === null ) {
638  return '';
639  }
640 
641  // XXX: include content model/type in feed item?
642  $revUser = $revisionRecord->getUser();
643  $revUserText = $revUser ? $revUser->getName() : '';
644  $revComment = $revisionRecord->getComment();
645  $revCommentText = $revComment ? $revComment->text : '';
646  return '<p>' . htmlspecialchars( $revUserText ) .
647  $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
648  htmlspecialchars( FeedItem::stripComment( $revCommentText ) ) .
649  "</p>\n<hr />\n<div>" .
650  nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
651  }
652 
653  private function canAnonymousUsersCreatePages() {
654  return $this->groupPermissionsLookup->groupHasPermission( '*', 'createpage' ) ||
655  $this->groupPermissionsLookup->groupHasPermission( '*', 'createtalk' );
656  }
657 
658  protected function getGroupName() {
659  return 'changes';
660  }
661 
662  protected function getCacheTTL() {
663  return 60 * 5;
664  }
665 }
getUser()
getAuthority()
const NS_USER
Definition: Defines.php:66
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
static formatSummaryRow( $tags, $unused, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:198
static factory( $displayFormat, $descriptor, IContextSource $context, $messagePrefix='')
Construct a HTMLForm object for given display type.
Definition: HTMLForm.php:352
Marks HTML that shouldn't be escaped.
Definition: HtmlArmor.php:30
Shortcut to construct an includable special page.
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
A base class for outputting syndication feeds (e.g.
Definition: FeedItem.php:40
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:41
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.
Definition: FormOptions.php:89
validateIntBounds( $name, $min, $max)
This class is a collection of static functions that serve two purposes:
Definition: Html.php:55
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
Page revision base class.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Represents a title within MediaWiki.
Definition: Title.php:82
Value object representing a user's identity.
Provides access to user options.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
A special page that list newly created pages.
__construct(LinkBatchFactory $linkBatchFactory, CommentStore $commentStore, IContentHandlerFactory $contentHandlerFactory, GroupPermissionsLookup $groupPermissionsLookup, ILoadBalancer $loadBalancer, RevisionLookup $revisionLookup, NamespaceInfo $namespaceInfo, UserOptionsLookup $userOptionsLookup, CommentFormatter $commentFormatter)
FormOptions $opts
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!
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.
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.
$self
Service for looking up page revisions.
This class is a delegate to ILBFactory for a given database cluster.
$content
Definition: router.php:76
if(!isset( $args[0])) $lang