1<?php
2/**
3 * Implements Special:PagesWithProp
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 * @since 1.21
21 * @file
22 * @ingroup SpecialPage
23 */
24
25use Wikimedia\Rdbms\ILoadBalancer;
26
27/**
28 * Special:PagesWithProp to search the page_props table
29 * @ingroup SpecialPage
30 * @since 1.21
31 */
32class SpecialPagesWithProp extends QueryPage {
33
34	/**
35	 * @var string|null
36	 */
37	private $propName = null;
38
39	/**
40	 * @var string[]|null
41	 */
42	private $existingPropNames = null;
43
44	/**
45	 * @var int|null
46	 */
47	private $ns;
48
49	/**
50	 * @var bool
51	 */
52	private $reverse = false;
53
54	/**
55	 * @var bool
56	 */
57	private $sortByValue = false;
58
59	/**
60	 * @param ILoadBalancer $loadBalancer
61	 */
62	public function __construct( ILoadBalancer $loadBalancer ) {
63		parent::__construct( 'PagesWithProp' );
64		$this->setDBLoadBalancer( $loadBalancer );
65	}
66
67	public function isCacheable() {
68		return false;
69	}
70
71	public function execute( $par ) {
72		$this->setHeaders();
73		$this->outputHeader();
74		$this->getOutput()->addModuleStyles( 'mediawiki.special' );
75
76		$request = $this->getRequest();
77		$propname = $request->getVal( 'propname', $par );
78		$this->ns = $request->getIntOrNull( 'namespace' );
79		$this->reverse = $request->getBool( 'reverse' );
80		$this->sortByValue = $request->getBool( 'sortbyvalue' );
81
82		$propnames = $this->getExistingPropNames();
83
84		$fields = [
85			'propname' => [
86				'type' => 'combobox',
87				'name' => 'propname',
88				'options' => $propnames,
89				'default' => $propname,
90				'label-message' => 'pageswithprop-prop',
91				'required' => true,
92			],
93			'namespace' => [
94				'type' => 'namespaceselect',
95				'name' => 'namespace',
96				'label-message' => 'namespace',
97				'all' => '',
98				'default' => $this->ns,
99			],
100			'reverse' => [
101				'type' => 'check',
102				'name' => 'reverse',
103				'default' => $this->reverse,
104				'label-message' => 'pageswithprop-reverse',
105				'required' => false,
106			],
107			'sortbyvalue' => [
108				'type' => 'check',
109				'name' => 'sortbyvalue',
110				'default' => $this->sortByValue,
111				'label-message' => 'pageswithprop-sortbyvalue',
112				'required' => false,
113			]
114		];
115
116		$context = new DerivativeContext( $this->getContext() );
117		$context->setTitle( $this->getPageTitle() ); // Remove subpage
118		$form = HTMLForm::factory( 'ooui', $fields, $context )
119			->setMethod( 'get' )
120			->setSubmitCallback( [ $this, 'onSubmit' ] )
121			->setWrapperLegendMsg( 'pageswithprop-legend' )
122			->addHeaderText( $this->msg( 'pageswithprop-text' )->parseAsBlock() )
123			->setSubmitTextMsg( 'pageswithprop-submit' )
124			->prepareForm();
125		$form->displayForm( false );
126		if ( $propname !== '' && $propname !== null ) {
127			$form->trySubmit();
128		}
129	}
130
131	public function onSubmit( $data, $form ) {
132		$this->propName = $data['propname'];
133		parent::execute( $data['propname'] );
134	}
135
136	/**
137	 * Return an array of subpages beginning with $search that this special page will accept.
138	 *
139	 * @param string $search Prefix to search for
140	 * @param int $limit Maximum number of results to return
141	 * @param int $offset Number of pages to skip
142	 * @return string[] Matching subpages
143	 */
144	public function prefixSearchSubpages( $search, $limit, $offset ) {
145		$subpages = array_keys( $this->queryExistingProps( $limit, $offset ) );
146		// We've already limited and offsetted, set to N and 0 respectively.
147		return self::prefixSearchArray( $search, count( $subpages ), $subpages, 0 );
148	}
149
150	/**
151	 * Disable RSS/Atom feeds
152	 * @return bool
153	 */
154	public function isSyndicated() {
155		return false;
156	}
157
158	/**
159	 * @inheritDoc
160	 */
161	protected function linkParameters() {
162		$params = [
163			'reverse' => $this->reverse,
164			'sortbyvalue' => $this->sortByValue,
165		];
166		if ( $this->ns !== null ) {
167			$params['namespace'] = $this->ns;
168		}
169		return $params;
170	}
171
172	public function getQueryInfo() {
173		$query = [
174			'tables' => [ 'page_props', 'page' ],
175			'fields' => [
176				'page_id' => 'pp_page',
177				'page_namespace',
178				'page_title',
179				'page_len',
180				'page_is_redirect',
181				'page_latest',
182				'pp_value',
183			],
184			'conds' => [
185				'pp_propname' => $this->propName,
186			],
187			'join_conds' => [
188				'page' => [ 'JOIN', 'page_id = pp_page' ]
189			],
190			'options' => []
191		];
192
193		if ( $this->ns !== null ) {
194			$query['conds']['page_namespace'] = $this->ns;
195		}
196
197		return $query;
198	}
199
200	protected function getOrderFields() {
201		$sort = [ 'page_id' ];
202		if ( $this->sortByValue ) {
203			array_unshift( $sort, 'pp_sortkey' );
204		}
205		return $sort;
206	}
207
208	/**
209	 * @return bool
210	 */
211	public function sortDescending() {
212		return !$this->reverse;
213	}
214
215	/**
216	 * @param Skin $skin
217	 * @param stdClass $result Result row
218	 * @return string
219	 */
220	public function formatResult( $skin, $result ) {
221		$title = Title::newFromRow( $result );
222		$ret = $this->getLinkRenderer()->makeKnownLink( $title );
223		if ( $result->pp_value !== '' ) {
224			// Do not show very long or binary values on the special page
225			$valueLength = strlen( $result->pp_value );
226			$isBinary = strpos( $result->pp_value, "\0" ) !== false;
227			$isTooLong = $valueLength > 1024;
228
229			if ( $isBinary || $isTooLong ) {
230				$message = $this
231					->msg( $isBinary ? 'pageswithprop-prophidden-binary' : 'pageswithprop-prophidden-long' )
232					->params( $this->getLanguage()->formatSize( $valueLength ) );
233
234				$propValue = Html::element( 'span', [ 'class' => 'prop-value-hidden' ], $message->text() );
235			} else {
236				$propValue = Html::element( 'span', [ 'class' => 'prop-value' ], $result->pp_value );
237			}
238
239			$ret .= $this->msg( 'colon-separator' )->escaped() . $propValue;
240		}
241
242		return $ret;
243	}
244
245	public function getExistingPropNames() {
246		if ( $this->existingPropNames === null ) {
247			$this->existingPropNames = $this->queryExistingProps();
248		}
249		return $this->existingPropNames;
250	}
251
252	protected function queryExistingProps( $limit = null, $offset = 0 ) {
253		$opts = [
254			'DISTINCT', 'ORDER BY' => 'pp_propname'
255		];
256		if ( $limit ) {
257			$opts['LIMIT'] = $limit;
258		}
259		if ( $offset ) {
260			$opts['OFFSET'] = $offset;
261		}
262
263		$dbr = $this->getDBLoadBalancer()->getConnectionRef( ILoadBalancer::DB_REPLICA );
264		$res = $dbr->select(
265			'page_props',
266			'pp_propname',
267			'',
268			__METHOD__,
269			$opts
270		);
271
272		$propnames = [];
273		foreach ( $res as $row ) {
274			$propnames[$row->pp_propname] = $row->pp_propname;
275		}
276
277		return $propnames;
278	}
279
280	protected function getGroupName() {
281		return 'pages';
282	}
283}
284