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