]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/specials/SpecialNewpages.php
MediaWiki 1.30.2
[autoinstallsdev/mediawiki.git] / includes / specials / SpecialNewpages.php
1 <?php
2 /**
3  * Implements Special:Newpages
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @ingroup SpecialPage
22  */
23
24 /**
25  * A special page that list newly created pages
26  *
27  * @ingroup SpecialPage
28  */
29 class SpecialNewpages extends IncludableSpecialPage {
30         /**
31          * @var FormOptions
32          */
33         protected $opts;
34         protected $customFilters;
35
36         protected $showNavigation = false;
37
38         public function __construct() {
39                 parent::__construct( 'Newpages' );
40         }
41
42         protected function setup( $par ) {
43                 // Options
44                 $opts = new FormOptions();
45                 $this->opts = $opts; // bind
46                 $opts->add( 'hideliu', false );
47                 $opts->add( 'hidepatrolled', $this->getUser()->getBoolOption( 'newpageshidepatrolled' ) );
48                 $opts->add( 'hidebots', false );
49                 $opts->add( 'hideredirs', true );
50                 $opts->add( 'limit', $this->getUser()->getIntOption( 'rclimit' ) );
51                 $opts->add( 'offset', '' );
52                 $opts->add( 'namespace', '0' );
53                 $opts->add( 'username', '' );
54                 $opts->add( 'feed', '' );
55                 $opts->add( 'tagfilter', '' );
56                 $opts->add( 'invert', false );
57                 $opts->add( 'size-mode', 'max' );
58                 $opts->add( 'size', 0 );
59
60                 $this->customFilters = [];
61                 Hooks::run( 'SpecialNewPagesFilters', [ $this, &$this->customFilters ] );
62                 foreach ( $this->customFilters as $key => $params ) {
63                         $opts->add( $key, $params['default'] );
64                 }
65
66                 // Set values
67                 $opts->fetchValuesFromRequest( $this->getRequest() );
68                 if ( $par ) {
69                         $this->parseParams( $par );
70                 }
71
72                 // Validate
73                 $opts->validateIntBounds( 'limit', 0, 5000 );
74         }
75
76         protected function parseParams( $par ) {
77                 $bits = preg_split( '/\s*,\s*/', trim( $par ) );
78                 foreach ( $bits as $bit ) {
79                         if ( 'shownav' == $bit ) {
80                                 $this->showNavigation = true;
81                         }
82                         if ( 'hideliu' === $bit ) {
83                                 $this->opts->setValue( 'hideliu', true );
84                         }
85                         if ( 'hidepatrolled' == $bit ) {
86                                 $this->opts->setValue( 'hidepatrolled', true );
87                         }
88                         if ( 'hidebots' == $bit ) {
89                                 $this->opts->setValue( 'hidebots', true );
90                         }
91                         if ( 'showredirs' == $bit ) {
92                                 $this->opts->setValue( 'hideredirs', false );
93                         }
94                         if ( is_numeric( $bit ) ) {
95                                 $this->opts->setValue( 'limit', intval( $bit ) );
96                         }
97
98                         $m = [];
99                         if ( preg_match( '/^limit=(\d+)$/', $bit, $m ) ) {
100                                 $this->opts->setValue( 'limit', intval( $m[1] ) );
101                         }
102                         // PG offsets not just digits!
103                         if ( preg_match( '/^offset=([^=]+)$/', $bit, $m ) ) {
104                                 $this->opts->setValue( 'offset', intval( $m[1] ) );
105                         }
106                         if ( preg_match( '/^username=(.*)$/', $bit, $m ) ) {
107                                 $this->opts->setValue( 'username', $m[1] );
108                         }
109                         if ( preg_match( '/^namespace=(.*)$/', $bit, $m ) ) {
110                                 $ns = $this->getLanguage()->getNsIndex( $m[1] );
111                                 if ( $ns !== false ) {
112                                         $this->opts->setValue( 'namespace', $ns );
113                                 }
114                         }
115                 }
116         }
117
118         /**
119          * Show a form for filtering namespace and username
120          *
121          * @param string $par
122          */
123         public function execute( $par ) {
124                 $out = $this->getOutput();
125
126                 $this->setHeaders();
127                 $this->outputHeader();
128
129                 $this->showNavigation = !$this->including(); // Maybe changed in setup
130                 $this->setup( $par );
131
132                 $this->addHelpLink( 'Help:New pages' );
133
134                 if ( !$this->including() ) {
135                         // Settings
136                         $this->form();
137
138                         $feedType = $this->opts->getValue( 'feed' );
139                         if ( $feedType ) {
140                                 $this->feed( $feedType );
141
142                                 return;
143                         }
144
145                         $allValues = $this->opts->getAllValues();
146                         unset( $allValues['feed'] );
147                         $out->setFeedAppendQuery( wfArrayToCgi( $allValues ) );
148                 }
149
150                 $pager = new NewPagesPager( $this, $this->opts );
151                 $pager->mLimit = $this->opts->getValue( 'limit' );
152                 $pager->mOffset = $this->opts->getValue( 'offset' );
153
154                 if ( $pager->getNumRows() ) {
155                         $navigation = '';
156                         if ( $this->showNavigation ) {
157                                 $navigation = $pager->getNavigationBar();
158                         }
159                         $out->addHTML( $navigation . $pager->getBody() . $navigation );
160                 } else {
161                         $out->addWikiMsg( 'specialpage-empty' );
162                 }
163         }
164
165         protected function filterLinks() {
166                 // show/hide links
167                 $showhide = [ $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() ];
168
169                 // Option value -> message mapping
170                 $filters = [
171                         'hideliu' => 'rcshowhideliu',
172                         'hidepatrolled' => 'rcshowhidepatr',
173                         'hidebots' => 'rcshowhidebots',
174                         'hideredirs' => 'whatlinkshere-hideredirs'
175                 ];
176                 foreach ( $this->customFilters as $key => $params ) {
177                         $filters[$key] = $params['msg'];
178                 }
179
180                 // Disable some if needed
181                 if ( !User::groupHasPermission( '*', 'createpage' ) ) {
182                         unset( $filters['hideliu'] );
183                 }
184                 if ( !$this->getUser()->useNPPatrol() ) {
185                         unset( $filters['hidepatrolled'] );
186                 }
187
188                 $links = [];
189                 $changed = $this->opts->getChangedValues();
190                 unset( $changed['offset'] ); // Reset offset if query type changes
191
192                 $self = $this->getPageTitle();
193                 $linkRenderer = $this->getLinkRenderer();
194                 foreach ( $filters as $key => $msg ) {
195                         $onoff = 1 - $this->opts->getValue( $key );
196                         $link = $linkRenderer->makeLink(
197                                 $self,
198                                 new HtmlArmor( $showhide[$onoff] ),
199                                 [],
200                                 [ $key => $onoff ] + $changed
201                         );
202                         $links[$key] = $this->msg( $msg )->rawParams( $link )->escaped();
203                 }
204
205                 return $this->getLanguage()->pipeList( $links );
206         }
207
208         protected function form() {
209                 $out = $this->getOutput();
210                 $out->addModules( 'mediawiki.userSuggest' );
211
212                 // Consume values
213                 $this->opts->consumeValue( 'offset' ); // don't carry offset, DWIW
214                 $namespace = $this->opts->consumeValue( 'namespace' );
215                 $username = $this->opts->consumeValue( 'username' );
216                 $tagFilterVal = $this->opts->consumeValue( 'tagfilter' );
217                 $nsinvert = $this->opts->consumeValue( 'invert' );
218
219                 $size = $this->opts->consumeValue( 'size' );
220                 $max = $this->opts->consumeValue( 'size-mode' ) === 'max';
221
222                 // Check username input validity
223                 $ut = Title::makeTitleSafe( NS_USER, $username );
224                 $userText = $ut ? $ut->getText() : '';
225
226                 // Store query values in hidden fields so that form submission doesn't lose them
227                 $hidden = [];
228                 foreach ( $this->opts->getUnconsumedValues() as $key => $value ) {
229                         $hidden[] = Html::hidden( $key, $value );
230                 }
231                 $hidden = implode( "\n", $hidden );
232
233                 $form = [
234                         'namespace' => [
235                                 'type' => 'namespaceselect',
236                                 'name' => 'namespace',
237                                 'label-message' => 'namespace',
238                                 'default' => $namespace,
239                         ],
240                         'nsinvert' => [
241                                 'type' => 'check',
242                                 'name' => 'invert',
243                                 'label-message' => 'invert',
244                                 'default' => $nsinvert,
245                                 'tooltip' => 'invert',
246                         ],
247                         'tagFilter' => [
248                                 'type' => 'tagfilter',
249                                 'name' => 'tagfilter',
250                                 'label-raw' => $this->msg( 'tag-filter' )->parse(),
251                                 'default' => $tagFilterVal,
252                         ],
253                         'username' => [
254                                 'type' => 'text',
255                                 'name' => 'username',
256                                 'label-message' => 'newpages-username',
257                                 'default' => $userText,
258                                 'id' => 'mw-np-username',
259                                 'size' => 30,
260                                 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
261                         ],
262                         'size' => [
263                                 'type' => 'sizefilter',
264                                 'name' => 'size',
265                                 'default' => -$max * $size,
266                         ],
267                 ];
268
269                 $htmlForm = new HTMLForm( $form, $this->getContext() );
270
271                 $htmlForm->setSubmitText( $this->msg( 'newpages-submit' )->text() );
272                 $htmlForm->setSubmitProgressive();
273                 // The form should be visible on each request (inclusive requests with submitted forms), so
274                 // return always false here.
275                 $htmlForm->setSubmitCallback(
276                         function () {
277                                 return false;
278                         }
279                 );
280                 $htmlForm->setMethod( 'get' );
281                 $htmlForm->setWrapperLegend( true );
282                 $htmlForm->setWrapperLegendMsg( 'newpages' );
283                 $htmlForm->addFooterText( Html::rawElement(
284                         'div',
285                         null,
286                         $this->filterLinks()
287                 ) );
288                 $htmlForm->show();
289         }
290
291         /**
292          * @param stdClass $result Result row from recent changes
293          * @return Revision|bool
294          */
295         protected function revisionFromRcResult( stdClass $result ) {
296                 return new Revision( [
297                         'comment' => CommentStore::newKey( 'rc_comment' )->getComment( $result )->text,
298                         'deleted' => $result->rc_deleted,
299                         'user_text' => $result->rc_user_text,
300                         'user' => $result->rc_user,
301                 ] );
302         }
303
304         /**
305          * Format a row, providing the timestamp, links to the page/history,
306          * size, user links, and a comment
307          *
308          * @param object $result Result row
309          * @return string
310          */
311         public function formatRow( $result ) {
312                 $title = Title::newFromRow( $result );
313
314                 // Revision deletion works on revisions,
315                 // so cast our recent change row to a revision row.
316                 $rev = $this->revisionFromRcResult( $result );
317                 $rev->setTitle( $title );
318
319                 $classes = [];
320                 $attribs = [ 'data-mw-revid' => $result->rev_id ];
321
322                 $lang = $this->getLanguage();
323                 $dm = $lang->getDirMark();
324
325                 $spanTime = Html::element( 'span', [ 'class' => 'mw-newpages-time' ],
326                         $lang->userTimeAndDate( $result->rc_timestamp, $this->getUser() )
327                 );
328                 $linkRenderer = $this->getLinkRenderer();
329                 $time = $linkRenderer->makeKnownLink(
330                         $title,
331                         new HtmlArmor( $spanTime ),
332                         [],
333                         [ 'oldid' => $result->rc_this_oldid ]
334                 );
335
336                 $query = $title->isRedirect() ? [ 'redirect' => 'no' ] : [];
337
338                 $plink = $linkRenderer->makeKnownLink(
339                         $title,
340                         null,
341                         [ 'class' => 'mw-newpages-pagename' ],
342                         $query
343                 );
344                 $histLink = $linkRenderer->makeKnownLink(
345                         $title,
346                         $this->msg( 'hist' )->text(),
347                         [],
348                         [ 'action' => 'history' ]
349                 );
350                 $hist = Html::rawElement( 'span', [ 'class' => 'mw-newpages-history' ],
351                         $this->msg( 'parentheses' )->rawParams( $histLink )->escaped() );
352
353                 $length = Html::rawElement(
354                         'span',
355                         [ 'class' => 'mw-newpages-length' ],
356                         $this->msg( 'brackets' )->rawParams(
357                                 $this->msg( 'nbytes' )->numParams( $result->length )->escaped()
358                         )->escaped()
359                 );
360
361                 $ulink = Linker::revUserTools( $rev );
362                 $comment = Linker::revComment( $rev );
363
364                 if ( $this->patrollable( $result ) ) {
365                         $classes[] = 'not-patrolled';
366                 }
367
368                 # Add a class for zero byte pages
369                 if ( $result->length == 0 ) {
370                         $classes[] = 'mw-newpages-zero-byte-page';
371                 }
372
373                 # Tags, if any.
374                 if ( isset( $result->ts_tags ) ) {
375                         list( $tagDisplay, $newClasses ) = ChangeTags::formatSummaryRow(
376                                 $result->ts_tags,
377                                 'newpages',
378                                 $this->getContext()
379                         );
380                         $classes = array_merge( $classes, $newClasses );
381                 } else {
382                         $tagDisplay = '';
383                 }
384
385                 # Display the old title if the namespace/title has been changed
386                 $oldTitleText = '';
387                 $oldTitle = Title::makeTitle( $result->rc_namespace, $result->rc_title );
388
389                 if ( !$title->equals( $oldTitle ) ) {
390                         $oldTitleText = $oldTitle->getPrefixedText();
391                         $oldTitleText = Html::rawElement(
392                                 'span',
393                                 [ 'class' => 'mw-newpages-oldtitle' ],
394                                 $this->msg( 'rc-old-title' )->params( $oldTitleText )->escaped()
395                         );
396                 }
397
398                 $ret = "{$time} {$dm}{$plink} {$hist} {$dm}{$length} {$dm}{$ulink} {$comment} "
399                         . "{$tagDisplay} {$oldTitleText}";
400
401                 // Let extensions add data
402                 Hooks::run( 'NewPagesLineEnding', [ $this, &$ret, $result, &$classes, &$attribs ] );
403                 $attribs = wfArrayFilterByKey( $attribs, [ Sanitizer::class, 'isReservedDataAttribute' ] );
404
405                 if ( count( $classes ) ) {
406                         $attribs['class'] = implode( ' ', $classes );
407                 }
408
409                 return Html::rawElement( 'li', $attribs, $ret ) . "\n";
410         }
411
412         /**
413          * Should a specific result row provide "patrollable" links?
414          *
415          * @param object $result Result row
416          * @return bool
417          */
418         protected function patrollable( $result ) {
419                 return ( $this->getUser()->useNPPatrol() && !$result->rc_patrolled );
420         }
421
422         /**
423          * Output a subscription feed listing recent edits to this page.
424          *
425          * @param string $type
426          */
427         protected function feed( $type ) {
428                 if ( !$this->getConfig()->get( 'Feed' ) ) {
429                         $this->getOutput()->addWikiMsg( 'feed-unavailable' );
430
431                         return;
432                 }
433
434                 $feedClasses = $this->getConfig()->get( 'FeedClasses' );
435                 if ( !isset( $feedClasses[$type] ) ) {
436                         $this->getOutput()->addWikiMsg( 'feed-invalid' );
437
438                         return;
439                 }
440
441                 $feed = new $feedClasses[$type](
442                         $this->feedTitle(),
443                         $this->msg( 'tagline' )->text(),
444                         $this->getPageTitle()->getFullURL()
445                 );
446
447                 $pager = new NewPagesPager( $this, $this->opts );
448                 $limit = $this->opts->getValue( 'limit' );
449                 $pager->mLimit = min( $limit, $this->getConfig()->get( 'FeedLimit' ) );
450
451                 $feed->outHeader();
452                 if ( $pager->getNumRows() > 0 ) {
453                         foreach ( $pager->mResult as $row ) {
454                                 $feed->outItem( $this->feedItem( $row ) );
455                         }
456                 }
457                 $feed->outFooter();
458         }
459
460         protected function feedTitle() {
461                 $desc = $this->getDescription();
462                 $code = $this->getConfig()->get( 'LanguageCode' );
463                 $sitename = $this->getConfig()->get( 'Sitename' );
464
465                 return "$sitename - $desc [$code]";
466         }
467
468         protected function feedItem( $row ) {
469                 $title = Title::makeTitle( intval( $row->rc_namespace ), $row->rc_title );
470                 if ( $title ) {
471                         $date = $row->rc_timestamp;
472                         $comments = $title->getTalkPage()->getFullURL();
473
474                         return new FeedItem(
475                                 $title->getPrefixedText(),
476                                 $this->feedItemDesc( $row ),
477                                 $title->getFullURL(),
478                                 $date,
479                                 $this->feedItemAuthor( $row ),
480                                 $comments
481                         );
482                 } else {
483                         return null;
484                 }
485         }
486
487         protected function feedItemAuthor( $row ) {
488                 return isset( $row->rc_user_text ) ? $row->rc_user_text : '';
489         }
490
491         protected function feedItemDesc( $row ) {
492                 $revision = Revision::newFromId( $row->rev_id );
493                 if ( !$revision ) {
494                         return '';
495                 }
496
497                 $content = $revision->getContent();
498                 if ( $content === null ) {
499                         return '';
500                 }
501
502                 // XXX: include content model/type in feed item?
503                 return '<p>' . htmlspecialchars( $revision->getUserText() ) .
504                         $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
505                         htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
506                         "</p>\n<hr />\n<div>" .
507                         nl2br( htmlspecialchars( $content->serialize() ) ) . "</div>";
508         }
509
510         protected function getGroupName() {
511                 return 'changes';
512         }
513
514         protected function getCacheTTL() {
515                 return 60 * 5;
516         }
517 }