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