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