Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 161
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialPrefixIndex
0.00% covered (danger)
0.00%
0 / 160
0.00% covered (danger)
0.00%
0 / 5
1122
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
110
 namespacePrefixForm
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
2
 showPrefixChunk
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 1
420
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Specials;
22
23use MediaWiki\Cache\LinkCache;
24use MediaWiki\Html\Html;
25use MediaWiki\HTMLForm\Field\HTMLCheckField;
26use MediaWiki\HTMLForm\HTMLForm;
27use MediaWiki\Title\Title;
28use Wikimedia\Rdbms\IConnectionProvider;
29use Wikimedia\Rdbms\IExpression;
30use Wikimedia\Rdbms\LikeValue;
31
32/**
33 * Implements Special:Prefixindex
34 *
35 * @ingroup SpecialPage
36 * @ingroup Search
37 */
38class SpecialPrefixIndex extends SpecialAllPages {
39
40    /**
41     * Whether to remove the searched prefix from the displayed link. Useful
42     * for inclusion of a set of subpages in a root page.
43     */
44    protected $stripPrefix = false;
45
46    protected $hideRedirects = false;
47
48    // Inherit $maxPerPage
49
50    // phpcs:ignore MediaWiki.Commenting.PropertyDocumentation.WrongStyle
51    private IConnectionProvider $dbProvider;
52    private LinkCache $linkCache;
53
54    /**
55     * @param IConnectionProvider $dbProvider
56     * @param LinkCache $linkCache
57     */
58    public function __construct(
59        IConnectionProvider $dbProvider,
60        LinkCache $linkCache
61    ) {
62        parent::__construct( $dbProvider );
63        $this->mName = 'Prefixindex';
64        $this->dbProvider = $dbProvider;
65        $this->linkCache = $linkCache;
66    }
67
68    /**
69     * Entry point: initialise variables and call subfunctions.
70     * @param string|null $par Becomes "FOO" when called like Special:Prefixindex/FOO
71     */
72    public function execute( $par ) {
73        $this->setHeaders();
74        $this->outputHeader();
75
76        $out = $this->getOutput();
77        $out->addModuleStyles( 'mediawiki.special' );
78
79        # GET values
80        $request = $this->getRequest();
81        $from = $request->getVal( 'from', '' );
82        $prefix = $request->getVal( 'prefix', '' );
83        $ns = $request->getIntOrNull( 'namespace' );
84        $namespace = (int)$ns; // if no namespace given, use 0 (NS_MAIN).
85        $this->hideRedirects = $request->getBool( 'hideredirects', $this->hideRedirects );
86        $this->stripPrefix = $request->getBool( 'stripprefix', $this->stripPrefix );
87
88        $namespaces = $this->getContentLanguage()->getNamespaces();
89        $out->setPageTitleMsg(
90            ( $namespace > 0 && array_key_exists( $namespace, $namespaces ) )
91                ? $this->msg( 'prefixindex-namespace' )->plaintextParams(
92                    str_replace( '_', ' ', $namespaces[$namespace] )
93                )
94                : $this->msg( 'prefixindex' )
95        );
96
97        $showme = '';
98        if ( $par !== null ) {
99            $showme = $par;
100        } elseif ( $prefix != '' ) {
101            $showme = $prefix;
102        } elseif ( $from != '' && $ns === null ) {
103            // For back-compat with Special:Allpages
104            // Don't do this if namespace is passed, so paging works when doing NS views.
105            $showme = $from;
106        }
107
108        // T29864: if transcluded, show all pages instead of the form.
109        if ( $this->including() || $showme != '' || $ns !== null ) {
110            $this->showPrefixChunk( $namespace, $showme, $from );
111        } else {
112            $out->addHTML( $this->namespacePrefixForm( $namespace, '' )->getHTML( false ) );
113        }
114    }
115
116    /**
117     * Prepared HTMLForm object for the top form
118     * @param int $namespace A namespace constant (default NS_MAIN).
119     * @param string $from DbKey we are starting listing at.
120     * @return HTMLForm
121     */
122    protected function namespacePrefixForm( $namespace = NS_MAIN, $from = '' ): HTMLForm {
123        $formDescriptor = [
124            'prefix' => [
125                'label-message' => 'allpagesprefix',
126                'name' => 'prefix',
127                'id' => 'nsfrom',
128                'type' => 'text',
129                'size' => '30',
130                'default' => str_replace( '_', ' ', $from ),
131            ],
132            'namespace' => [
133                'type' => 'namespaceselect',
134                'name' => 'namespace',
135                'id' => 'namespace',
136                'label-message' => 'namespace',
137                'all' => null,
138                'default' => $namespace,
139            ],
140            'hidedirects' => [
141                'class' => HTMLCheckField::class,
142                'name' => 'hideredirects',
143                'label-message' => 'allpages-hide-redirects',
144            ],
145            'stripprefix' => [
146                'class' => HTMLCheckField::class,
147                'name' => 'stripprefix',
148                'label-message' => 'prefixindex-strip',
149            ],
150        ];
151
152        $this->getHookRunner()->onSpecialPrefixIndexGetFormFilters( $this->getContext(), $formDescriptor );
153
154        $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
155            ->setMethod( 'get' )
156            ->setTitle( $this->getPageTitle() ) // Remove subpage
157            ->setWrapperLegendMsg( 'prefixindex' )
158            ->setSubmitTextMsg( 'prefixindex-submit' );
159
160        return $htmlForm->prepareForm();
161    }
162
163    /**
164     * @param int $namespace
165     * @param string $prefix
166     * @param string|null $from List all pages from this name (default false)
167     */
168    protected function showPrefixChunk( $namespace, $prefix, $from = null ) {
169        if ( $from === null ) {
170            $from = $prefix;
171        }
172
173        $fromList = $this->getNamespaceKeyAndText( $namespace, $from );
174        $prefixList = $this->getNamespaceKeyAndText( $namespace, $prefix );
175        $namespaces = $this->getContentLanguage()->getNamespaces();
176        $res = null;
177        $n = 0;
178        $nextRow = null;
179        $preparedHtmlForm = $this->namespacePrefixForm( $namespace, $prefix );
180
181        if ( !$prefixList || !$fromList ) {
182            $out = $this->msg( 'allpagesbadtitle' )->parseAsBlock();
183        } elseif ( !array_key_exists( $namespace, $namespaces ) ) {
184            // Show errormessage and reset to NS_MAIN
185            $out = $this->msg( 'allpages-bad-ns', $namespace )->parse();
186            $namespace = NS_MAIN;
187        } else {
188            [ $namespace, $prefixKey, $prefix ] = $prefixList;
189            [ /* $fromNS */, $fromKey, ] = $fromList;
190
191            # ## @todo FIXME: Should complain if $fromNs != $namespace
192
193            $dbr = $this->dbProvider->getReplicaDatabase();
194            $queryBuiler = $dbr->newSelectQueryBuilder()
195                ->select( LinkCache::getSelectFields() )
196                ->from( 'page' )
197                ->where( [
198                    'page_namespace' => $namespace,
199                    $dbr->expr(
200                        'page_title',
201                        IExpression::LIKE,
202                        new LikeValue( $prefixKey, $dbr->anyString() )
203                    ),
204                    $dbr->expr( 'page_title', '>=', $fromKey ),
205                ] )
206                ->orderBy( 'page_title' )
207                ->limit( $this->maxPerPage + 1 )
208                ->useIndex( 'page_name_title' );
209
210            if ( $this->hideRedirects ) {
211                $queryBuiler->andWhere( [ 'page_is_redirect' => 0 ] );
212            }
213
214            $this->getHookRunner()->onSpecialPrefixIndexQuery( $preparedHtmlForm->mFieldData, $queryBuiler );
215
216            $res = $queryBuiler->caller( __METHOD__ )->fetchResultSet();
217
218            // @todo FIXME: Side link to previous
219
220            if ( $res->numRows() > 0 ) {
221                $out = Html::openElement( 'ul', [ 'class' => 'mw-prefixindex-list' ] );
222
223                $prefixLength = strlen( $prefix );
224                foreach ( $res as $row ) {
225                    if ( $n >= $this->maxPerPage ) {
226                        $nextRow = $row;
227                        break;
228                    }
229                    $title = Title::newFromRow( $row );
230                    // Make sure it gets into LinkCache
231                    $this->linkCache->addGoodLinkObjFromRow( $title, $row );
232                    $displayed = $title->getText();
233                    // Try not to generate unclickable links
234                    if ( $this->stripPrefix && $prefixLength !== strlen( $displayed ) ) {
235                        $displayed = substr( $displayed, $prefixLength );
236                    }
237                    $link = ( $title->isRedirect() ? '<div class="allpagesredirect">' : '' ) .
238                        $this->getLinkRenderer()->makeKnownLink(
239                            $title,
240                            $displayed
241                        ) .
242                        ( $title->isRedirect() ? '</div>' : '' );
243
244                    $out .= "<li>$link</li>\n";
245                    $n++;
246
247                }
248                $out .= Html::closeElement( 'ul' );
249
250                if ( $res->numRows() > 2 ) {
251                    // Only apply CSS column styles if there are more than 2 entries.
252                    // Otherwise, rendering is broken as "mw-prefixindex-body"'s CSS column count is 3.
253                    $out = Html::rawElement( 'div', [ 'class' => 'mw-prefixindex-body' ], $out );
254                }
255            } else {
256                $out = '';
257            }
258        }
259
260        $output = $this->getOutput();
261
262        if ( $this->including() ) {
263            // We don't show the nav-links and the form when included in other
264            // pages, so let's just finish here.
265            $output->addHTML( $out );
266            return;
267        }
268
269        $topOut = $preparedHtmlForm->getHTML( false );
270
271        if ( $res && ( $n == $this->maxPerPage ) && $nextRow ) {
272            $query = [
273                'from' => $nextRow->page_title,
274                'prefix' => $prefix,
275                'hideredirects' => $this->hideRedirects,
276                'stripprefix' => $this->stripPrefix,
277            ];
278
279            if ( $namespace || $prefix == '' ) {
280                // Keep the namespace even if it's 0 for empty prefixes.
281                // This tells us we're not just a holdover from old links.
282                $query['namespace'] = $namespace;
283            }
284
285            $nextLink = $this->getLinkRenderer()->makeKnownLink(
286                $this->getPageTitle(),
287                $this->msg( 'nextpage', str_replace( '_', ' ', $nextRow->page_title ) )->text(),
288                [],
289                $query
290            );
291
292            // Link shown at the top of the page below the form
293            $topOut .= Html::rawElement( 'div',
294                [ 'class' => 'mw-prefixindex-nav' ],
295                $nextLink
296            );
297
298            // Link shown at the footer
299            $out .= "\n" . Html::element( 'hr' ) .
300                Html::rawElement(
301                    'div',
302                    [ 'class' => 'mw-prefixindex-nav' ],
303                    $nextLink
304                );
305
306        }
307
308        $output->addHTML( $topOut . $out );
309    }
310
311    protected function getGroupName() {
312        return 'pages';
313    }
314}
315
316/**
317 * Retain the old class name for backwards compatibility.
318 * @deprecated since 1.41
319 */
320class_alias( SpecialPrefixIndex::class, 'SpecialPrefixindex' );