Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 172 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
| CargoPageValues | |
0.00% |
0 / 172 |
|
0.00% |
0 / 12 |
2652 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 69 |
|
0.00% |
0 / 1 |
420 | |||
| getTableLink | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
| getInfoForAllFields | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
| getRowsForPageInTable | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
| printRow | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
| printTable | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| tocIndent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| tocLine | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
| tocLineEnd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| tocList | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
2 | |||
| isListed | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Displays an interface to let users recreate data via the Cargo |
| 4 | * extension. |
| 5 | * |
| 6 | * @author Yaron Koren |
| 7 | * @ingroup Cargo |
| 8 | */ |
| 9 | |
| 10 | use MediaWiki\Html\Html; |
| 11 | use MediaWiki\MediaWikiServices; |
| 12 | use MediaWiki\Page\PageReferenceValue; |
| 13 | use MediaWiki\Title\Title; |
| 14 | |
| 15 | class CargoPageValues extends IncludableSpecialPage { |
| 16 | public $mTitle; |
| 17 | |
| 18 | public function __construct( $title = null ) { |
| 19 | parent::__construct( 'PageValues' ); |
| 20 | |
| 21 | $this->mTitle = $title; |
| 22 | } |
| 23 | |
| 24 | public function execute( $subpage = null ) { |
| 25 | if ( $subpage ) { |
| 26 | // Allow inclusion with e.g. {{Special:PageValues/Book}} |
| 27 | $this->mTitle = Title::newFromText( $subpage ); |
| 28 | } |
| 29 | |
| 30 | // If no title, or a nonexistent title, was set, just exit out. |
| 31 | // @TODO - display an error message. |
| 32 | if ( $this->mTitle == null || !$this->mTitle->exists() ) { |
| 33 | return true; |
| 34 | } |
| 35 | |
| 36 | $out = $this->getOutput(); |
| 37 | |
| 38 | $this->setHeaders(); |
| 39 | |
| 40 | $pageName = $this->mTitle->getPrefixedText(); |
| 41 | $out->setPageTitle( $this->msg( 'cargo-pagevaluesfor', $pageName )->text() ); |
| 42 | |
| 43 | $pageRef = PageReferenceValue::localReference( NS_SPECIAL, "PageValues/$pageName" ); |
| 44 | MediaWikiServices::getInstance()->getParser()->setPage( $pageRef ); |
| 45 | |
| 46 | $text = ''; |
| 47 | |
| 48 | $tableNames = []; |
| 49 | |
| 50 | $cdb = CargoUtils::getDB(); |
| 51 | if ( $cdb->tableExists( '_pageData__NEXT', __METHOD__ ) ) { |
| 52 | $tableNames[] = '_pageData__NEXT'; |
| 53 | } elseif ( $cdb->tableExists( '_pageData', __METHOD__ ) ) { |
| 54 | $tableNames[] = '_pageData'; |
| 55 | } |
| 56 | if ( $cdb->tableExists( '_fileData__NEXT', __METHOD__ ) ) { |
| 57 | $tableNames[] = '_fileData__NEXT'; |
| 58 | } elseif ( $cdb->tableExists( '_fileData', __METHOD__ ) ) { |
| 59 | $tableNames[] = '_fileData'; |
| 60 | } |
| 61 | |
| 62 | $dbr = CargoUtils::getMainDBForRead(); |
| 63 | $res = $dbr->select( |
| 64 | 'cargo_pages', 'table_name', |
| 65 | [ 'page_id' => $this->mTitle->getArticleID() ], |
| 66 | __METHOD__ |
| 67 | ); |
| 68 | foreach ( $res as $row ) { |
| 69 | $tableNames[] = $row->table_name; |
| 70 | } |
| 71 | |
| 72 | $toc = self::tocIndent(); |
| 73 | $tocLength = 0; |
| 74 | |
| 75 | foreach ( $tableNames as $tableName ) { |
| 76 | try { |
| 77 | $queryResults = $this->getRowsForPageInTable( $tableName ); |
| 78 | } catch ( Exception $e ) { |
| 79 | // Most likely this is because the _pageData |
| 80 | // table doesn't exist. |
| 81 | continue; |
| 82 | } |
| 83 | $numRowsOnPage = count( $queryResults ); |
| 84 | |
| 85 | // Hide _fileData if it's empty - we do this only for _fileData, |
| 86 | // as another table having 0 rows can indicate an error, and we'd |
| 87 | // like to preserve that information for debugging purposes. |
| 88 | if ( $numRowsOnPage === 0 && ( $tableName === '_fileData' || $tableName === '_fileData__NEXT' ) ) { |
| 89 | continue; |
| 90 | } |
| 91 | |
| 92 | $tableLink = $this->getTableLink( $tableName ); |
| 93 | |
| 94 | $tableSectionHeader = $this->msg( 'cargo-pagevalues-tablevalues' )->rawParams( $tableLink )->escaped(); |
| 95 | $tableSectionTocDisplay = $this->msg( 'cargo-pagevalues-tablevalues', $tableName )->escaped(); |
| 96 | $tableSectionAnchor = $this->msg( 'cargo-pagevalues-tablevalues', $tableName )->escaped(); |
| 97 | $tableSectionAnchor = Sanitizer::escapeIdForAttribute( $tableSectionAnchor ); |
| 98 | |
| 99 | // We construct the table of contents at the same time |
| 100 | // as the main text. |
| 101 | $toc .= self::tocLine( $tableSectionAnchor, $tableSectionTocDisplay, |
| 102 | $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . self::tocLineEnd(); |
| 103 | |
| 104 | $h2 = Html::rawElement( 'h2', [], |
| 105 | Html::rawElement( 'span', [ 'class' => 'mw-headline', 'id' => $tableSectionAnchor ], $tableSectionHeader ) ); |
| 106 | |
| 107 | $text .= Html::rawElement( 'div', [ 'class' => 'cargo-pagevalues-tableinfo' ], |
| 108 | $h2 . $this->msg( "cargo-pagevalues-tableinfo-numrows", $numRowsOnPage ) |
| 109 | ); |
| 110 | |
| 111 | foreach ( $queryResults as $rowValues ) { |
| 112 | $tableContents = ''; |
| 113 | $fieldInfo = $this->getInfoForAllFields( $tableName ); |
| 114 | $anyFieldHasAllowedValues = false; |
| 115 | foreach ( $fieldInfo as $info ) { |
| 116 | if ( $info['allowed values'] !== '' ) { |
| 117 | $anyFieldHasAllowedValues = true; |
| 118 | } |
| 119 | } |
| 120 | foreach ( $rowValues as $field => $value ) { |
| 121 | // @HACK - this check should ideally |
| 122 | // be done earlier. |
| 123 | if ( strpos( $field, '__precision' ) !== false ) { |
| 124 | continue; |
| 125 | } |
| 126 | $tableContents .= $this->printRow( $field, $value, $fieldInfo[$field], $anyFieldHasAllowedValues ); |
| 127 | } |
| 128 | $text .= $this->printTable( $tableContents, $anyFieldHasAllowedValues ); |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Show table of contents only if there are enough sections. |
| 133 | if ( count( $tableNames ) >= 3 ) { |
| 134 | $toc = self::tocList( $toc ); |
| 135 | $out->addHTML( $toc ); |
| 136 | } |
| 137 | |
| 138 | $out->addHTML( $text ); |
| 139 | $out->addModules( 'ext.cargo.main' ); |
| 140 | $out->addModuleStyles( 'ext.cargo.pagevalues' ); |
| 141 | |
| 142 | return true; |
| 143 | } |
| 144 | |
| 145 | private function getTableLink( $tableName ) { |
| 146 | $originalTableName = str_replace( '__NEXT', '', $tableName ); |
| 147 | $isReplacementTable = substr( $tableName, -6 ) == '__NEXT'; |
| 148 | $viewURL = SpecialPage::getTitleFor( 'CargoTables' )->getFullURL() . "/$originalTableName"; |
| 149 | if ( $isReplacementTable ) { |
| 150 | $viewURL .= strpos( $viewURL, '?' ) ? '&' : '?'; |
| 151 | $viewURL .= "_replacement"; |
| 152 | } |
| 153 | |
| 154 | return Html::element( 'a', [ 'href' => $viewURL ], $tableName ); |
| 155 | } |
| 156 | |
| 157 | /** |
| 158 | * Used to get the information about field type and the list |
| 159 | * of allowed values (if any) of all fields of a table. |
| 160 | * |
| 161 | * @param string $tableName |
| 162 | */ |
| 163 | private function getInfoForAllFields( $tableName ) { |
| 164 | $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] ); |
| 165 | if ( $tableName == '_pageData' || $tableName == '_pageData__NEXT' ) { |
| 166 | CargoUtils::addGlobalFieldsToSchema( $tableSchemas[$tableName] ); |
| 167 | } |
| 168 | $fieldDescriptions = $tableSchemas[$tableName]->mFieldDescriptions; |
| 169 | $fieldInfo = []; |
| 170 | foreach ( $fieldDescriptions as $fieldName => $fieldDescription ) { |
| 171 | $fieldInfo[$fieldName]['field type'] = $fieldDescription->prettyPrintType(); |
| 172 | if ( is_array( $fieldDescription->mAllowedValues ) ) { |
| 173 | $fieldInfo[$fieldName]['allowed values'] = $fieldDescription->prettyPrintAllowedValues(); |
| 174 | } else { |
| 175 | $fieldInfo[$fieldName]['allowed values'] = ''; |
| 176 | } |
| 177 | } |
| 178 | return $fieldInfo; |
| 179 | } |
| 180 | |
| 181 | public function getRowsForPageInTable( $tableName ) { |
| 182 | $cdb = CargoUtils::getDB(); |
| 183 | |
| 184 | $sqlQuery = new CargoSQLQuery(); |
| 185 | $sqlQuery->mAliasedTableNames = [ $tableName => $tableName ]; |
| 186 | |
| 187 | $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] ); |
| 188 | |
| 189 | if ( $tableName == '_pageData' || $tableName == '_pageData__NEXT' ) { |
| 190 | CargoUtils::addGlobalFieldsToSchema( $tableSchemas[$tableName] ); |
| 191 | } |
| 192 | |
| 193 | $sqlQuery->mTableSchemas = $tableSchemas; |
| 194 | |
| 195 | $aliasedFieldNames = []; |
| 196 | foreach ( $tableSchemas[$tableName]->mFieldDescriptions as $fieldName => $fieldDescription ) { |
| 197 | if ( $fieldDescription->mIsHidden ) { |
| 198 | // @TODO - do some custom formatting |
| 199 | } |
| 200 | |
| 201 | // $fieldAlias = str_replace( '_', ' ', $fieldName ); |
| 202 | $fieldAlias = $fieldName; |
| 203 | |
| 204 | if ( $fieldDescription->mIsList ) { |
| 205 | $aliasedFieldNames[$fieldAlias] = $fieldName . '__full'; |
| 206 | } elseif ( $fieldDescription->mType == 'Coordinates' ) { |
| 207 | $aliasedFieldNames[$fieldAlias] = $fieldName . '__full'; |
| 208 | } else { |
| 209 | $aliasedFieldNames[$fieldAlias] = $fieldName; |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | $sqlQuery->mAliasedFieldNames = $aliasedFieldNames; |
| 214 | $sqlQuery->mOrigAliasedFieldNames = $aliasedFieldNames; |
| 215 | $sqlQuery->setDescriptionsAndTableNamesForFields(); |
| 216 | $sqlQuery->handleDateFields(); |
| 217 | $sqlQuery->mWhereStr = $cdb->addIdentifierQuotes( '_pageID' ) . " = " . |
| 218 | $this->mTitle->getArticleID(); |
| 219 | |
| 220 | $queryResults = $sqlQuery->run(); |
| 221 | $queryDisplayer = CargoQueryDisplayer::newFromSQLQuery( $sqlQuery ); |
| 222 | $formattedQueryResults = $queryDisplayer->getFormattedQueryResults( $queryResults ); |
| 223 | return $formattedQueryResults; |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * Based on MediaWiki's InfoAction::addRow() |
| 228 | */ |
| 229 | public function printRow( $name, $value, $fieldInfo, $fieldHasAnyAllowedValues ) { |
| 230 | if ( $name == '_fullText' && strlen( $value ) > 300 ) { |
| 231 | $value = substr( $value, 0, 300 ) . ' ...'; |
| 232 | } |
| 233 | $text = Html::element( 'td', [ 'class' => 'cargo-pagevalues-table-field' ], $name ) . |
| 234 | Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-type' ], $fieldInfo['field type'] ); |
| 235 | if ( $fieldHasAnyAllowedValues ) { |
| 236 | $allowedValuesText = $fieldInfo['allowed values']; |
| 237 | // Count "middot" as only one character, not eight, when counting the string length. |
| 238 | $allowedValuesDisplayText = str_replace( '·', '.', $allowedValuesText ); |
| 239 | if ( strlen( $allowedValuesDisplayText ) > 25 ) { |
| 240 | $allowedValuesText = '<span class="cargoMinimizedText">' . $fieldInfo['allowed values'] . '</span>'; |
| 241 | } |
| 242 | $text .= Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-allowedvalues' ], $allowedValuesText ); |
| 243 | } |
| 244 | $text .= Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-value' ], $value ); |
| 245 | |
| 246 | return Html::rawElement( 'tr', [], $text ); |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Based on MediaWiki's InfoAction::addTable() |
| 251 | */ |
| 252 | public function printTable( $tableContents, $anyFieldHasAllowedValues ) { |
| 253 | $headerRow = Html::element( 'th', [], $this->msg( 'cargo-field' )->text() ) . |
| 254 | Html::element( 'th', [], $this->msg( 'cargo-field-type' )->text() ); |
| 255 | if ( $anyFieldHasAllowedValues ) { |
| 256 | $headerRow .= Html::element( 'th', [], $this->msg( 'cargo-allowed-values' )->text() ); |
| 257 | } |
| 258 | $headerRow .= Html::element( 'th', [], $this->msg( 'cargo-value' )->text() ); |
| 259 | $headerRow = Html::rawElement( 'tr', [], $headerRow ); |
| 260 | return Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ], |
| 261 | $headerRow . $tableContents ) . "\n"; |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Add another level to the Table of Contents |
| 266 | * |
| 267 | * Copied from HandleTOCMarkers::tocIndent(), which is unfortunately private. |
| 268 | * |
| 269 | * @return string |
| 270 | */ |
| 271 | private static function tocIndent() { |
| 272 | return "\n<ul>\n"; |
| 273 | } |
| 274 | |
| 275 | /** |
| 276 | * parameter level defines if we are on an indentation level |
| 277 | * |
| 278 | * Copied from HandleTOCMarkers::tocLine(), which is unfortunately private. |
| 279 | * |
| 280 | * @param string $linkAnchor Identifier |
| 281 | * @param string $tocline Properly escaped HTML |
| 282 | * @param string $tocnumber Unescaped text |
| 283 | * @param int $level |
| 284 | * @param string|false $sectionIndex |
| 285 | * @return string |
| 286 | */ |
| 287 | private static function tocLine( $linkAnchor, $tocline, $tocnumber, $level, $sectionIndex = false ) { |
| 288 | $classes = "toclevel-$level"; |
| 289 | // Parser.php used to suppress tocLine by setting $sectionindex to false. |
| 290 | // In those circumstances, we can now encounter '' or a "T-" prefixed index |
| 291 | // for when the section comes from templates. |
| 292 | if ( $sectionIndex !== false && $sectionIndex !== '' && !str_starts_with( $sectionIndex, "T-" ) ) { |
| 293 | $classes .= " tocsection-$sectionIndex"; |
| 294 | } |
| 295 | // <li class="$classes"><a href="#$linkAnchor"><span class="tocnumber"> |
| 296 | // $tocnumber</span> <span class="toctext">$tocline</span></a> |
| 297 | return Html::openElement( 'li', [ 'class' => $classes ] ) |
| 298 | . Html::rawElement( 'a', |
| 299 | [ 'href' => "#$linkAnchor" ], |
| 300 | Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber ) |
| 301 | . ' ' |
| 302 | . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline ) |
| 303 | ); |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * End a Table Of Contents line. |
| 308 | * tocUnindent() will be used instead if we're ending a line below |
| 309 | * the new level. |
| 310 | * |
| 311 | * Copied from HandleTOCMarkers::tocLineEnd(), which is unfortunately private. |
| 312 | * |
| 313 | * @return string |
| 314 | */ |
| 315 | private static function tocLineEnd() { |
| 316 | return "</li>\n"; |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript. |
| 321 | * |
| 322 | * Copied from HandleTOCMarkers::tocList(), which is unfortunately private. |
| 323 | * |
| 324 | * @param string $toc Html of the Table Of Contents |
| 325 | * @param Language|null $lang Language for the toc title, defaults to user language |
| 326 | * @return string Full html of the TOC |
| 327 | */ |
| 328 | private static function tocList( $toc, ?Language $lang = null ) { |
| 329 | $lang ??= RequestContext::getMain()->getLanguage(); |
| 330 | $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped(); |
| 331 | return '<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">' |
| 332 | . Html::element( 'input', [ |
| 333 | 'type' => 'checkbox', |
| 334 | 'role' => 'button', |
| 335 | 'id' => 'toctogglecheckbox', |
| 336 | 'class' => 'toctogglecheckbox', |
| 337 | 'style' => 'display:none', |
| 338 | ] ) |
| 339 | . Html::openElement( 'div', [ |
| 340 | 'class' => 'toctitle', |
| 341 | 'lang' => $lang->getHtmlCode(), |
| 342 | 'dir' => $lang->getDir(), |
| 343 | ] ) |
| 344 | . '<h2 id="mw-toc-heading">' . $title . '</h2>' |
| 345 | . '<span class="toctogglespan">' |
| 346 | . Html::label( '', 'toctogglecheckbox', [ |
| 347 | 'class' => 'toctogglelabel', |
| 348 | ] ) |
| 349 | . '</span>' |
| 350 | . '</div>' |
| 351 | . $toc |
| 352 | . "</ul>\n</div>\n"; |
| 353 | } |
| 354 | |
| 355 | /** |
| 356 | * Don't list this in Special:SpecialPages. |
| 357 | */ |
| 358 | public function isListed() { |
| 359 | return false; |
| 360 | } |
| 361 | } |