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