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