1<?php 2/** 3 * This program is free software; you can redistribute it and/or modify 4 * it under the terms of the GNU General Public License as published by 5 * the Free Software Foundation; either version 2 of the License, or 6 * (at your option) any later version. 7 * 8 * This program is distributed in the hope that it will be useful, 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 * GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along 14 * with this program; if not, write to the Free Software Foundation, Inc., 15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 16 * http://www.gnu.org/copyleft/gpl.html 17 * 18 * @file 19 * @ingroup Pager 20 */ 21 22use MediaWiki\Linker\LinkRenderer; 23use Wikimedia\Rdbms\FakeResultWrapper; 24use Wikimedia\Rdbms\IDatabase; 25use Wikimedia\Rdbms\ILoadBalancer; 26use Wikimedia\Rdbms\IResultWrapper; 27 28/** 29 * Use TablePager for prettified output. We have to pretend that we're 30 * getting data from a table when in fact not all of it comes from the database. 31 * 32 * @ingroup Pager 33 */ 34class AllMessagesTablePager extends TablePager { 35 36 /** 37 * @var string 38 */ 39 protected $langcode; 40 41 /** 42 * @var bool 43 */ 44 protected $foreign; 45 46 /** 47 * @var string 48 */ 49 protected $prefix; 50 51 /** 52 * @var string 53 */ 54 protected $suffix; 55 56 /** 57 * @var Language 58 */ 59 public $lang; 60 61 /** 62 * @var null|bool 63 */ 64 public $custom; 65 66 /** @var LocalisationCache */ 67 private $localisationCache; 68 69 /** 70 * @param IContextSource|null $context 71 * @param FormOptions $opts 72 * @param LinkRenderer $linkRenderer 73 * @param Language $contentLanguage 74 * @param LocalisationCache $localisationCache 75 * @param ILoadBalancer $loadBalancer 76 */ 77 public function __construct( 78 ?IContextSource $context, 79 FormOptions $opts, 80 LinkRenderer $linkRenderer, 81 Language $contentLanguage, 82 LocalisationCache $localisationCache, 83 ILoadBalancer $loadBalancer 84 ) { 85 // Set database before parent constructor to avoid setting it there with wfGetDB 86 $this->mDb = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA ); 87 parent::__construct( $context, $linkRenderer ); 88 $this->localisationCache = $localisationCache; 89 90 $this->mIndexField = 'am_title'; 91 // FIXME: Why does this need to be set to DIR_DESCENDING to produce ascending ordering? 92 $this->mDefaultDirection = IndexPager::DIR_DESCENDING; 93 94 $this->lang = wfGetLangObj( $opts->getValue( 'lang' ) ); 95 96 $this->langcode = $this->lang->getCode(); 97 $this->foreign = !$this->lang->equals( $contentLanguage ); 98 99 $filter = $opts->getValue( 'filter' ); 100 if ( $filter === 'all' ) { 101 $this->custom = null; // So won't match in either case 102 } else { 103 $this->custom = ( $filter === 'unmodified' ); 104 } 105 106 $prefix = $this->getLanguage()->ucfirst( $opts->getValue( 'prefix' ) ); 107 $prefix = $prefix !== '' ? 108 Title::makeTitleSafe( NS_MEDIAWIKI, $opts->getValue( 'prefix' ) ) : 109 null; 110 111 if ( $prefix !== null ) { 112 $displayPrefix = $prefix->getDBkey(); 113 $this->prefix = '/^' . preg_quote( $displayPrefix, '/' ) . '/i'; 114 } else { 115 $this->prefix = false; 116 } 117 118 // The suffix that may be needed for message names if we're in a 119 // different language (eg [[MediaWiki:Foo/fr]]: $suffix = '/fr' 120 if ( $this->foreign ) { 121 $this->suffix = '/' . $this->langcode; 122 } else { 123 $this->suffix = ''; 124 } 125 } 126 127 private function getAllMessages( $descending ) { 128 $messageNames = $this->localisationCache->getSubitemList( 'en', 'messages' ); 129 130 // Normalise message names so they look like page titles and sort correctly - T86139 131 $messageNames = array_map( [ $this->lang, 'ucfirst' ], $messageNames ); 132 133 if ( $descending ) { 134 rsort( $messageNames ); 135 } else { 136 asort( $messageNames ); 137 } 138 139 return $messageNames; 140 } 141 142 /** 143 * Determine which of the MediaWiki and MediaWiki_talk namespace pages exist. 144 * Returns [ 'pages' => ..., 'talks' => ... ], where the subarrays have 145 * an entry for each existing page, with the key being the message name and 146 * value arbitrary. 147 * 148 * @since 1.36 Added $dbr parameter 149 * 150 * @param array $messageNames 151 * @param string $langcode What language code 152 * @param bool $foreign Whether the $langcode is not the content language 153 * @param IDatabase|null $dbr 154 * @return array A 'pages' and 'talks' array with the keys of existing pages 155 */ 156 public static function getCustomisedStatuses( 157 $messageNames, 158 $langcode = 'en', 159 $foreign = false, 160 IDatabase $dbr = null 161 ) { 162 // FIXME: This function should be moved to Language:: or something. 163 // Fallback to global state, if not provided 164 $dbr = $dbr ?? wfGetDB( DB_REPLICA ); 165 $res = $dbr->select( 'page', 166 [ 'page_namespace', 'page_title' ], 167 [ 'page_namespace' => [ NS_MEDIAWIKI, NS_MEDIAWIKI_TALK ] ], 168 __METHOD__, 169 [ 'USE INDEX' => 'page_name_title' ] 170 ); 171 $xNames = array_fill_keys( $messageNames, true ); 172 173 $pageFlags = $talkFlags = []; 174 175 foreach ( $res as $s ) { 176 $exists = false; 177 178 if ( $foreign ) { 179 $titleParts = explode( '/', $s->page_title ); 180 if ( count( $titleParts ) === 2 && 181 $langcode === $titleParts[1] && 182 isset( $xNames[$titleParts[0]] ) 183 ) { 184 $exists = $titleParts[0]; 185 } 186 } elseif ( isset( $xNames[$s->page_title] ) ) { 187 $exists = $s->page_title; 188 } 189 190 $title = Title::newFromRow( $s ); 191 if ( $exists && $title->inNamespace( NS_MEDIAWIKI ) ) { 192 $pageFlags[$exists] = true; 193 } elseif ( $exists && $title->inNamespace( NS_MEDIAWIKI_TALK ) ) { 194 $talkFlags[$exists] = true; 195 } 196 } 197 198 return [ 'pages' => $pageFlags, 'talks' => $talkFlags ]; 199 } 200 201 /** 202 * This function normally does a database query to get the results; we need 203 * to make a pretend result using a FakeResultWrapper. 204 * @param string $offset 205 * @param int $limit 206 * @param bool $order 207 * @return IResultWrapper 208 */ 209 public function reallyDoQuery( $offset, $limit, $order ) { 210 $asc = ( $order === self::QUERY_ASCENDING ); 211 212 $messageNames = $this->getAllMessages( $order ); 213 $statuses = self::getCustomisedStatuses( 214 $messageNames, 215 $this->langcode, 216 $this->foreign, 217 $this->getDatabase() 218 ); 219 220 $rows = []; 221 $count = 0; 222 foreach ( $messageNames as $key ) { 223 $customised = isset( $statuses['pages'][$key] ); 224 if ( $customised !== $this->custom && 225 ( $asc && ( $key < $offset || !$offset ) || !$asc && $key > $offset ) && 226 ( ( $this->prefix && preg_match( $this->prefix, $key ) ) || $this->prefix === false ) 227 ) { 228 $actual = $this->msg( $key )->inLanguage( $this->lang )->plain(); 229 $default = $this->msg( $key )->inLanguage( $this->lang )->useDatabase( false )->plain(); 230 $rows[] = [ 231 'am_title' => $key, 232 'am_actual' => $actual, 233 'am_default' => $default, 234 'am_customised' => $customised, 235 'am_talk_exists' => isset( $statuses['talks'][$key] ) 236 ]; 237 $count++; 238 } 239 240 if ( $count === $limit ) { 241 break; 242 } 243 } 244 245 return new FakeResultWrapper( $rows ); 246 } 247 248 protected function getStartBody() { 249 $tableClass = $this->getTableClass(); 250 return Xml::openElement( 'table', [ 251 'class' => "mw-datatable $tableClass", 252 'id' => 'mw-allmessagestable' 253 ] ) . 254 "\n" . 255 "<thead><tr> 256 <th rowspan=\"2\">" . 257 $this->msg( 'allmessagesname' )->escaped() . " 258 </th> 259 <th>" . 260 $this->msg( 'allmessagesdefault' )->escaped() . 261 "</th> 262 </tr>\n 263 <tr> 264 <th>" . 265 $this->msg( 'allmessagescurrent' )->escaped() . 266 "</th> 267 </tr></thead>\n"; 268 } 269 270 protected function getEndBody() { 271 return Html::closeElement( 'table' ); 272 } 273 274 /** 275 * @param string $field 276 * @param string|null $value 277 * @return string HTML 278 */ 279 public function formatValue( $field, $value ) { 280 $linkRenderer = $this->getLinkRenderer(); 281 switch ( $field ) { 282 case 'am_title': 283 $title = Title::makeTitle( NS_MEDIAWIKI, $value . $this->suffix ); 284 $talk = Title::makeTitle( NS_MEDIAWIKI_TALK, $value . $this->suffix ); 285 $translation = Linker::makeExternalLink( 286 'https://translatewiki.net/w/i.php?' . wfArrayToCgi( [ 287 'title' => 'Special:SearchTranslations', 288 'group' => 'mediawiki', 289 'grouppath' => 'mediawiki', 290 'language' => $this->getLanguage()->getCode(), 291 'query' => $value . ' ' . $this->msg( $value )->plain() 292 ] ), 293 $this->msg( 'allmessages-filter-translate' )->text() 294 ); 295 $talkLink = $this->msg( 'talkpagelinktext' )->text(); 296 297 if ( $this->mCurrentRow->am_customised ) { 298 $title = $linkRenderer->makeKnownLink( $title, $this->getLanguage()->lcfirst( $value ) ); 299 } else { 300 $title = $linkRenderer->makeBrokenLink( 301 $title, $this->getLanguage()->lcfirst( $value ) 302 ); 303 } 304 if ( $this->mCurrentRow->am_talk_exists ) { 305 $talk = $linkRenderer->makeKnownLink( $talk, $talkLink ); 306 } else { 307 $talk = $linkRenderer->makeBrokenLink( 308 $talk, 309 $talkLink 310 ); 311 } 312 313 return $title . ' ' . 314 $this->msg( 'parentheses' )->rawParams( $talk )->escaped() . 315 ' ' . 316 $this->msg( 'parentheses' )->rawParams( $translation )->escaped(); 317 318 case 'am_default': 319 case 'am_actual': 320 return Sanitizer::escapeHtmlAllowEntities( $value ); 321 } 322 323 return ''; 324 } 325 326 /** 327 * @param stdClass $row 328 * @return string HTML 329 */ 330 public function formatRow( $row ) { 331 // Do all the normal stuff 332 $s = parent::formatRow( $row ); 333 334 // But if there's a customised message, add that too. 335 if ( $row->am_customised ) { 336 $s .= Html::openElement( 'tr', $this->getRowAttrs( $row ) ); 337 $formatted = strval( $this->formatValue( 'am_actual', $row->am_actual ) ); 338 339 if ( $formatted === '' ) { 340 $formatted = "\u{00A0}"; 341 } 342 343 $s .= Html::rawElement( 'td', $this->getCellAttrs( 'am_actual', $row->am_actual ), $formatted ) 344 . Html::closeElement( 'tr' ); 345 } 346 347 return Html::rawElement( 'tbody', [], $s ); 348 } 349 350 protected function getRowAttrs( $row ) { 351 return []; 352 } 353 354 /** 355 * @param string $field 356 * @param string $value 357 * @return array HTML attributes 358 */ 359 protected function getCellAttrs( $field, $value ) { 360 $attr = []; 361 if ( $field === 'am_title' ) { 362 if ( $this->mCurrentRow->am_customised ) { 363 $attr += [ 'rowspan' => '2' ]; 364 } 365 } else { 366 $attr += [ 367 'lang' => $this->lang->getHtmlCode(), 368 'dir' => $this->lang->getDir(), 369 ]; 370 if ( $this->mCurrentRow->am_customised ) { 371 // CSS class: am_default, am_actual 372 $attr += [ 'class' => $field ]; 373 } 374 } 375 return $attr; 376 } 377 378 // This is not actually used, as getStartBody is overridden above 379 protected function getFieldNames() { 380 return [ 381 'am_title' => $this->msg( 'allmessagesname' )->text(), 382 'am_default' => $this->msg( 'allmessagesdefault' )->text() 383 ]; 384 } 385 386 public function getTitle() { 387 return SpecialPage::getTitleFor( 'Allmessages', false ); 388 } 389 390 protected function isFieldSortable( $x ) { 391 return false; 392 } 393 394 public function getDefaultSort() { 395 return ''; 396 } 397 398 public function getQueryInfo() { 399 return []; 400 } 401 402} 403