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