Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 191
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialAllPages
0.00% covered (danger)
0.00%
0 / 190
0.00% covered (danger)
0.00%
0 / 8
2550
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 outputHTMLForm
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
6
 showToplevel
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
56
 showChunk
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 1
600
 getNamespaceKeyAndText
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Implements Special:Allpages
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
24namespace MediaWiki\Specials;
25
26use MediaWiki\Html\Html;
27use MediaWiki\HTMLForm\HTMLForm;
28use MediaWiki\MainConfigNames;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Page\ExistingPageRecord;
31use MediaWiki\Page\PageStore;
32use MediaWiki\SpecialPage\IncludableSpecialPage;
33use MediaWiki\Title\Title;
34use MediaWiki\Title\TitleValue;
35use SearchEngineFactory;
36use Wikimedia\Rdbms\IConnectionProvider;
37use Wikimedia\Rdbms\SelectQueryBuilder;
38
39/**
40 * Implements Special:Allpages
41 *
42 * @ingroup SpecialPage
43 * @todo Rewrite using IndexPager
44 */
45class SpecialAllPages extends IncludableSpecialPage {
46
47    /**
48     * Maximum number of pages to show on single subpage.
49     *
50     * @var int
51     */
52    protected $maxPerPage = 345;
53
54    /**
55     * Determines, which message describes the input field 'nsfrom'.
56     *
57     * @var string
58     */
59    protected $nsfromMsg = 'allpagesfrom';
60
61    private IConnectionProvider $dbProvider;
62    private SearchEngineFactory $searchEngineFactory;
63    private PageStore $pageStore;
64
65    public function __construct(
66        IConnectionProvider $dbProvider = null,
67        SearchEngineFactory $searchEngineFactory = null,
68        PageStore $pageStore = null
69    ) {
70        parent::__construct( 'Allpages' );
71        // This class is extended and therefore falls back to global state - T265309
72        $services = MediaWikiServices::getInstance();
73        $this->dbProvider = $dbProvider ?? $services->getConnectionProvider();
74        $this->searchEngineFactory = $searchEngineFactory ?? $services->getSearchEngineFactory();
75        $this->pageStore = $pageStore ?? $services->getPageStore();
76    }
77
78    /**
79     * Entry point : initialise variables and call subfunctions.
80     *
81     * @param string|null $par Becomes "FOO" when called like Special:Allpages/FOO
82     */
83    public function execute( $par ) {
84        $request = $this->getRequest();
85        $out = $this->getOutput();
86
87        $this->setHeaders();
88        $this->outputHeader();
89        $out->setPreventClickjacking( false );
90
91        # GET values
92        $from = $request->getVal( 'from', null );
93        $to = $request->getVal( 'to', null );
94        $namespace = $request->getInt( 'namespace' );
95
96        $miserMode = (bool)$this->getConfig()->get( MainConfigNames::MiserMode );
97
98        // Redirects filter is disabled in MiserMode
99        $hideredirects = $request->getBool( 'hideredirects', false ) && !$miserMode;
100
101        $namespaces = $this->getLanguage()->getNamespaces();
102
103        $out->setPageTitleMsg(
104            ( $namespace > 0 && array_key_exists( $namespace, $namespaces ) ) ?
105                $this->msg( 'allinnamespace' )->plaintextParams( str_replace( '_', ' ', $namespaces[$namespace] ) ) :
106                $this->msg( 'allarticles' )
107        );
108        $out->addModuleStyles( 'mediawiki.special' );
109
110        if ( $par !== null ) {
111            $this->showChunk( $namespace, $par, $to, $hideredirects );
112        } elseif ( $from !== null && $to === null ) {
113            $this->showChunk( $namespace, $from, $to, $hideredirects );
114        } else {
115            $this->showToplevel( $namespace, $from, $to, $hideredirects );
116        }
117    }
118
119    /**
120     * Outputs the HTMLForm used on this page
121     *
122     * @param int $namespace A namespace constant (default NS_MAIN).
123     * @param string $from DbKey we are starting listing at.
124     * @param string $to DbKey we are ending listing at.
125     * @param bool $hideRedirects Don't show redirects  (default false)
126     */
127    protected function outputHTMLForm( $namespace = NS_MAIN,
128        $from = '', $to = '', $hideRedirects = false
129    ) {
130        $miserMode = (bool)$this->getConfig()->get( MainConfigNames::MiserMode );
131        $formDescriptor = [
132            'from' => [
133                'type' => 'text',
134                'name' => 'from',
135                'id' => 'nsfrom',
136                'size' => 30,
137                'label-message' => 'allpagesfrom',
138                'default' => str_replace( '_', ' ', $from ),
139            ],
140            'to' => [
141                'type' => 'text',
142                'name' => 'to',
143                'id' => 'nsto',
144                'size' => 30,
145                'label-message' => 'allpagesto',
146                'default' => str_replace( '_', ' ', $to ),
147            ],
148            'namespace' => [
149                'type' => 'namespaceselect',
150                'name' => 'namespace',
151                'id' => 'namespace',
152                'label-message' => 'namespace',
153                'all' => null,
154                'default' => $namespace,
155            ],
156            'hideredirects' => [
157                'type' => 'check',
158                'name' => 'hideredirects',
159                'id' => 'hidredirects',
160                'label-message' => 'allpages-hide-redirects',
161                'value' => $hideRedirects,
162            ],
163        ];
164
165        if ( $miserMode ) {
166            unset( $formDescriptor['hideredirects'] );
167        }
168
169        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
170        $htmlForm
171            ->setMethod( 'get' )
172            ->setTitle( $this->getPageTitle() ) // Remove subpage
173            ->setWrapperLegendMsg( 'allpages' )
174            ->setSubmitTextMsg( 'allpagessubmit' )
175            ->prepareForm()
176            ->displayForm( false );
177    }
178
179    /**
180     * @param int $namespace (default NS_MAIN)
181     * @param string|null $from List all pages from this name
182     * @param string|null $to List all pages to this name
183     * @param bool $hideredirects Don't show redirects (default false)
184     */
185    private function showToplevel(
186        $namespace = NS_MAIN, $from = null, $to = null, $hideredirects = false
187    ) {
188        $from = $from ? Title::makeTitleSafe( $namespace, $from ) : null;
189        $to = $to ? Title::makeTitleSafe( $namespace, $to ) : null;
190        $from = ( $from && $from->isLocal() ) ? $from->getDBkey() : null;
191        $to = ( $to && $to->isLocal() ) ? $to->getDBkey() : null;
192
193        $this->showChunk( $namespace, $from, $to, $hideredirects );
194    }
195
196    /**
197     * @param int $namespace Namespace (Default NS_MAIN)
198     * @param string|null $from List all pages from this name (default null)
199     * @param string|null $to List all pages to this name (default null)
200     * @param bool $hideredirects Don't show redirects (default false)
201     */
202    private function showChunk(
203        $namespace = NS_MAIN, $from = null, $to = null, $hideredirects = false
204    ) {
205        $output = $this->getOutput();
206
207        $fromList = $this->getNamespaceKeyAndText( $namespace, $from );
208        $toList = $this->getNamespaceKeyAndText( $namespace, $to );
209        $namespaces = $this->getContext()->getLanguage()->getNamespaces();
210        $n = 0;
211        $prevTitle = null;
212
213        if ( !$fromList || !$toList ) {
214            $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock();
215        } elseif ( !array_key_exists( $namespace, $namespaces ) ) {
216            // Show errormessage and reset to NS_MAIN
217            $out = $this->msg( 'allpages-bad-ns', $namespace )->parse();
218            $namespace = NS_MAIN;
219        } else {
220            [ $namespace, $fromKey, $from ] = $fromList;
221            [ , $toKey, $to ] = $toList;
222
223            $dbr = $this->dbProvider->getReplicaDatabase();
224            $filterConds = [ 'page_namespace' => $namespace ];
225            if ( $hideredirects ) {
226                $filterConds['page_is_redirect'] = 0;
227            }
228
229            $conds = $filterConds;
230            $conds[] = $dbr->expr( 'page_title', '>=', $fromKey );
231            if ( $toKey !== "" ) {
232                $conds[] = $dbr->expr( 'page_title', '<=', $toKey );
233            }
234
235            $res = $this->pageStore->newSelectQueryBuilder()
236                ->where( $conds )
237                ->caller( __METHOD__ )
238                ->orderBy( 'page_title' )
239                ->limit( $this->maxPerPage + 1 )
240                ->useIndex( 'page_name_title' )
241                ->fetchPageRecords();
242
243            // Eagerly fetch the set of pages to be displayed and warm up LinkCache (T328174).
244            // Note that we can't use fetchPageRecordArray() here as that returns an array keyed
245            // by page IDs; we need a simple sequence.
246            /** @var ExistingPageRecord[] $pages */
247            $pages = iterator_to_array( $res );
248
249            $linkRenderer = $this->getLinkRenderer();
250            if ( count( $pages ) > 0 ) {
251                $out = Html::openElement( 'ul', [ 'class' => 'mw-allpages-chunk' ] );
252
253                while ( $n < $this->maxPerPage && $n < count( $pages ) ) {
254                    $page = $pages[$n];
255                    $attributes = $page->isRedirect() ? [ 'class' => 'allpagesredirect' ] : [];
256
257                    $out .= Html::rawElement( 'li', $attributes, $linkRenderer->makeKnownLink( $page ) ) . "\n";
258                    $n++;
259                }
260                $out .= Html::closeElement( 'ul' );
261
262                if ( count( $pages ) > 2 ) {
263                    // Only apply CSS column styles if there's more than 2 entries.
264                    // Otherwise, rendering is broken as "mw-allpages-body"'s CSS column count is 3.
265                    $out = Html::rawElement( 'div', [ 'class' => 'mw-allpages-body' ], $out );
266                }
267            } else {
268                $out = '';
269            }
270
271            if ( $fromKey !== '' && !$this->including() ) {
272                # Get the first title from previous chunk
273                $prevConds = $filterConds;
274                $prevConds[] = $dbr->expr( 'page_title', '<', $fromKey );
275                $prevKey = $dbr->newSelectQueryBuilder()
276                    ->select( 'page_title' )
277                    ->from( 'page' )
278                    ->where( $prevConds )
279                    ->orderBy( 'page_title', SelectQueryBuilder::SORT_DESC )
280                    ->offset( $this->maxPerPage - 1 )
281                    ->caller( __METHOD__ )->fetchField();
282
283                if ( $prevKey === false ) {
284                    # The previous chunk is not complete, need to link to the very first title
285                    # available in the database
286                    $prevKey = $dbr->newSelectQueryBuilder()
287                        ->select( 'page_title' )
288                        ->from( 'page' )
289                        ->where( $prevConds )
290                        ->orderBy( 'page_title' )
291                        ->caller( __METHOD__ )->fetchField();
292                }
293
294                if ( $prevKey !== false ) {
295                    $prevTitle = Title::makeTitle( $namespace, $prevKey );
296                }
297            }
298        }
299
300        if ( $this->including() ) {
301            $output->addHTML( $out );
302            return;
303        }
304
305        $navLinks = [];
306        $self = $this->getPageTitle();
307
308        $linkRenderer = $this->getLinkRenderer();
309        // Generate a "previous page" link if needed
310        if ( $prevTitle ) {
311            $query = [ 'from' => $prevTitle->getText() ];
312
313            if ( $namespace ) {
314                $query['namespace'] = $namespace;
315            }
316
317            if ( $hideredirects ) {
318                $query['hideredirects'] = $hideredirects;
319            }
320
321            $navLinks[] = $linkRenderer->makeKnownLink(
322                $self,
323                $this->msg( 'prevpage', $prevTitle->getText() )->text(),
324                [],
325                $query
326            );
327
328        }
329
330        // Generate a "next page" link if needed
331        if ( $n === $this->maxPerPage && isset( $pages[$n] ) ) {
332            # $t is the first link of the next chunk
333            $t = TitleValue::newFromPage( $pages[$n] );
334            $query = [ 'from' => $t->getText() ];
335
336            if ( $namespace ) {
337                $query['namespace'] = $namespace;
338            }
339
340            if ( $hideredirects ) {
341                $query['hideredirects'] = $hideredirects;
342            }
343
344            $navLinks[] = $linkRenderer->makeKnownLink(
345                $self,
346                $this->msg( 'nextpage', $t->getText() )->text(),
347                [],
348                $query
349            );
350        }
351
352        $this->outputHTMLForm( $namespace, $from, $to, $hideredirects );
353
354        if ( count( $navLinks ) ) {
355            // Add pagination links
356            $pagination = Html::rawElement( 'div',
357                [ 'class' => 'mw-allpages-nav' ],
358                $this->getLanguage()->pipeList( $navLinks )
359            );
360
361            $output->addHTML( $pagination );
362            $out .= Html::element( 'hr' ) . $pagination; // Footer
363        }
364
365        $output->addHTML( $out );
366    }
367
368    /**
369     * @param int $ns The namespace of the article
370     * @param string $text The name of the article
371     * @return array|null [ int namespace, string dbkey, string pagename ] or null on error
372     */
373    protected function getNamespaceKeyAndText( $ns, $text ) {
374        if ( $text == '' ) {
375            # shortcut for common case
376            return [ $ns, '', '' ];
377        }
378
379        $t = Title::makeTitleSafe( $ns, $text );
380        if ( $t && $t->isLocal() ) {
381            return [ $t->getNamespace(), $t->getDBkey(), $t->getText() ];
382        } elseif ( $t ) {
383            return null;
384        }
385
386        # try again, in case the problem was an empty pagename
387        $text = preg_replace( '/(#|$)/', 'X$1', $text );
388        $t = Title::makeTitleSafe( $ns, $text );
389        if ( $t && $t->isLocal() ) {
390            return [ $t->getNamespace(), '', '' ];
391        } else {
392            return null;
393        }
394    }
395
396    /**
397     * Return an array of subpages beginning with $search that this special page will accept.
398     *
399     * @param string $search Prefix to search for
400     * @param int $limit Maximum number of results to return (usually 10)
401     * @param int $offset Number of results to skip (usually 0)
402     * @return string[] Matching subpages
403     */
404    public function prefixSearchSubpages( $search, $limit, $offset ) {
405        return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory );
406    }
407
408    protected function getGroupName() {
409        return 'pages';
410    }
411}
412
413/** @deprecated class alias since 1.41 */
414class_alias( SpecialAllPages::class, 'SpecialAllPages' );