Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 428 |
|
0.00% |
0 / 14 |
CRAP | |
0.00% |
0 / 1 |
| CargoTables | |
0.00% |
0 / 428 |
|
0.00% |
0 / 14 |
7482 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 129 |
|
0.00% |
0 / 1 |
600 | |||
| displayNumRowsForTable | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
| displayNumColumnsForTable | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| getTableLinkedToView | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| getActionButton | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
| getActionIcon | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
| setAllowedActions | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
20 | |||
| deriveListOfColumnsFromUserAllowedActions | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| getActionLinksForTable | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
240 | |||
| tableTemplatesText | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
42 | |||
| displayListOfTables | |
0.00% |
0 / 143 |
|
0.00% |
0 / 1 |
462 | |||
| displayActionLinks | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * Defines a special page that shows the contents of a single table in |
| 4 | * the Cargo database. |
| 5 | * |
| 6 | * @author Yaron Koren |
| 7 | * @author Megan Cutrofello |
| 8 | * @ingroup Cargo |
| 9 | */ |
| 10 | |
| 11 | use MediaWiki\Html\Html; |
| 12 | use MediaWiki\MediaWikiServices; |
| 13 | use MediaWiki\Title\Title; |
| 14 | |
| 15 | class CargoTables extends IncludableSpecialPage { |
| 16 | |
| 17 | private $templatesThatDeclareTables; |
| 18 | private $templatesThatAttachToTables; |
| 19 | |
| 20 | private static $actionList = null; |
| 21 | |
| 22 | /** |
| 23 | * Constructor |
| 24 | */ |
| 25 | public function __construct() { |
| 26 | parent::__construct( 'CargoTables' ); |
| 27 | $this->templatesThatDeclareTables = CargoUtils::getAllPageProps( 'CargoTableName' ); |
| 28 | $this->templatesThatAttachToTables = CargoUtils::getAllPageProps( 'CargoAttachedTable' ); |
| 29 | } |
| 30 | |
| 31 | public function execute( $tableName ) { |
| 32 | $out = $this->getOutput(); |
| 33 | $req = $this->getRequest(); |
| 34 | $user = $this->getUser(); |
| 35 | $lang = $this->getLanguage(); |
| 36 | $this->setHeaders(); |
| 37 | |
| 38 | $out->addModules( [ |
| 39 | 'ext.cargo.main', |
| 40 | 'ext.cargo.cargotables', |
| 41 | ] ); |
| 42 | $out->addModuleStyles( [ |
| 43 | 'mediawiki.pager.styles' |
| 44 | ] ); |
| 45 | |
| 46 | if ( $tableName == '' ) { |
| 47 | $out->addHTML( $this->displayListOfTables() ); |
| 48 | |
| 49 | return; |
| 50 | } |
| 51 | |
| 52 | if ( !CargoUtils::tableFullyExists( $tableName ) ) { |
| 53 | $out->addHTML( Html::element( 'div', [ 'class' => 'error' ], |
| 54 | $this->msg( "cargo-unknowntable", $tableName )->escaped() ) ); |
| 55 | |
| 56 | return; |
| 57 | } |
| 58 | |
| 59 | $ctURL = SpecialPage::getTitleFor( 'CargoTables' )->getFullURL(); |
| 60 | $viewURL = "$ctURL/$tableName"; |
| 61 | |
| 62 | if ( $req->getCheck( '_replacement' ) ) { |
| 63 | $pageTitle = |
| 64 | $this->msg( 'cargo-cargotables-viewreplacement', '"' . $tableName . '"' )->escaped(); |
| 65 | $tableLink = Html::element( 'a', [ 'href' => $viewURL ], $tableName ); |
| 66 | $text = $this->msg( 'cargo-cargotables-replacementtable' )->rawParams( $tableLink )->escaped(); |
| 67 | if ( $user->isAllowed( 'recreatecargodata' ) ) { |
| 68 | $switchURL = |
| 69 | SpecialPage::getTitleFor( 'SwitchCargoTable' )->getFullURL() . "/$tableName"; |
| 70 | $text .= ' ' . Html::element( 'a', [ 'href' => $switchURL ], |
| 71 | $this->msg( "cargo-cargotables-switch" )->escaped() ); |
| 72 | |
| 73 | if ( $user->isAllowed( 'deletecargodata' ) ) { |
| 74 | $deleteURL = |
| 75 | SpecialPage::getTitleFor( 'DeleteCargoTable' )->getFullURL() . |
| 76 | "/$tableName"; |
| 77 | $deleteURL .= strpos( $deleteURL, '?' ) ? '&' : '?'; |
| 78 | $deleteURL .= "_replacement"; |
| 79 | $text .= ' ' . |
| 80 | $this->msg( 'cargo-cargotables-deletereplacement', $deleteURL )->parse(); |
| 81 | } |
| 82 | } |
| 83 | $out->addHtml( Html::warningBox( $text ) ); |
| 84 | $tableName .= '__NEXT'; |
| 85 | } else { |
| 86 | $pageTitle = $this->msg( 'cargo-cargotables-viewtable', $tableName )->escaped(); |
| 87 | if ( CargoUtils::tableFullyExists( $tableName . '__NEXT' ) ) { |
| 88 | $text = Html::warningBox( $this->msg( 'cargo-cargotables-hasreplacement' )->escaped() ); |
| 89 | $out->addHtml( $text ); |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | $out->setPageTitle( $pageTitle ); |
| 94 | |
| 95 | // Mimic the appearance of a subpage to link back to |
| 96 | // Special:CargoTables. |
| 97 | $ctPage = CargoUtils::getSpecialPage( 'CargoTables' ); |
| 98 | $mainPageLink = |
| 99 | CargoUtils::makeLink( $this->getLinkRenderer(), $ctPage->getPageTitle(), |
| 100 | htmlspecialchars( $ctPage->getDescription() ) ); |
| 101 | $out->setSubtitle( '< ' . $mainPageLink ); |
| 102 | |
| 103 | $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] ); |
| 104 | $fieldDescriptions = $tableSchemas[$tableName]->mFieldDescriptions; |
| 105 | |
| 106 | // Display the table structure. |
| 107 | $structureDesc = '<p>' . $this->msg( 'cargo-cargotables-tablestructure' )->escaped() . '</p>'; |
| 108 | $structureDesc .= '<ol>'; |
| 109 | foreach ( $fieldDescriptions as $fieldName => $fieldDescription ) { |
| 110 | $fieldDesc = '<strong>' . $fieldName . '</strong> - '; |
| 111 | $fieldDesc .= $fieldDescription->prettyPrintTypeAndAttributes(); |
| 112 | $structureDesc .= Html::rawElement( 'li', [], $fieldDesc ) . "\n"; |
| 113 | } |
| 114 | $structureDesc .= '</ol>'; |
| 115 | $out->addHTML( $structureDesc ); |
| 116 | |
| 117 | // Then, display a count. |
| 118 | $cdb = CargoUtils::getDB(); |
| 119 | $numRows = $cdb->selectRowCount( $tableName, '*', [], __METHOD__ ); |
| 120 | $numRowsMessage = |
| 121 | $this->msg( 'cargo-cargotables-totalrows' )->numParams( $numRows )->parse(); |
| 122 | $out->addWikiTextAsInterface( $numRowsMessage . "\n" ); |
| 123 | |
| 124 | // Display "Recreate data" link. |
| 125 | if ( array_key_exists( $tableName, $this->templatesThatDeclareTables ) ) { |
| 126 | $templatesThatDeclareThisTable = $this->templatesThatDeclareTables[$tableName]; |
| 127 | } |
| 128 | if ( isset( $templatesThatDeclareThisTable ) && count( $templatesThatDeclareThisTable ) > 0 ) { |
| 129 | // Most likely, there's only one. |
| 130 | $templateID = $templatesThatDeclareThisTable[0]; |
| 131 | $templateTitle = Title::newFromID( $templateID ); |
| 132 | $recreateDataURL = $templateTitle->getLocalURL( [ 'action' => 'recreatedata' ] ); |
| 133 | $recreateDataMessage = $this->msg( 'recreatedata' )->escaped(); |
| 134 | $recreateDataLink = Html::element( 'a', [ 'href' => $recreateDataURL ], $recreateDataMessage ); |
| 135 | $out->addHTML( '<p>' . $recreateDataLink . '.</p>' ); |
| 136 | } |
| 137 | |
| 138 | // Then, show the actual table, via a query. |
| 139 | $sqlQuery = new CargoSQLQuery(); |
| 140 | $sqlQuery->mTablesStr = $tableName; |
| 141 | $sqlQuery->mAliasedTableNames = [ $tableName => $tableName ]; |
| 142 | |
| 143 | $sqlQuery->mTableSchemas = $tableSchemas; |
| 144 | |
| 145 | $pageNameFieldAlias = $this->msg( 'nstab-main' )->parse(); |
| 146 | foreach ( $fieldDescriptions as $fieldName => $fieldDescription ) { |
| 147 | $fieldAlias = str_replace( '_', ' ', $fieldName ); |
| 148 | if ( strcasecmp( $fieldAlias, $pageNameFieldAlias ) === 0 ) { |
| 149 | $pageNameFieldAlias = '_pageName'; |
| 150 | break; |
| 151 | } |
| 152 | } |
| 153 | $aliasedFieldNames = [ $pageNameFieldAlias => '_pageName' ]; |
| 154 | foreach ( $fieldDescriptions as $fieldName => $fieldDescription ) { |
| 155 | // Skip "hidden" fields. |
| 156 | if ( property_exists( $fieldDescription, 'hidden' ) ) { |
| 157 | continue; |
| 158 | } |
| 159 | |
| 160 | if ( $fieldName[0] != '_' ) { |
| 161 | $fieldAlias = str_replace( '_', ' ', $fieldName ); |
| 162 | } else { |
| 163 | $fieldAlias = $fieldName; |
| 164 | } |
| 165 | $fieldType = $fieldDescription->mType; |
| 166 | // Special handling for URLs, to avoid them |
| 167 | // overwhelming the page. |
| 168 | // @TODO - something similar should be done for lists |
| 169 | // of URLs. |
| 170 | if ( $fieldType == 'URL' && !$fieldDescription->mIsList ) { |
| 171 | $quotedFieldName = $cdb->addIdentifierQuotes( $fieldName ); |
| 172 | // Thankfully, there's a message in core |
| 173 | // MediaWiki that seems to just be "URL". |
| 174 | $fieldName = |
| 175 | "CONCAT('[', $quotedFieldName, ' " . |
| 176 | $this->msg( 'version-entrypoints-header-url' )->parse() . "]')"; |
| 177 | // Only display this if the URL is not blank. |
| 178 | // Unfortunately, IF is not consistently |
| 179 | // supported in PostgreSQL - just leave this |
| 180 | // logic out if we're using Postgres. |
| 181 | if ( $cdb->getType() !== 'postgres' ) { |
| 182 | $fieldName = "IF ($quotedFieldName <> '', $fieldName, '')"; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | if ( $fieldDescription->mIsList ) { |
| 187 | $aliasedFieldNames[$fieldAlias] = $fieldName . '__full'; |
| 188 | } elseif ( $fieldType == 'Coordinates' ) { |
| 189 | $aliasedFieldNames[$fieldAlias] = $fieldName . '__full'; |
| 190 | } else { |
| 191 | $aliasedFieldNames[$fieldAlias] = $fieldName; |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | $sqlQuery->mAliasedFieldNames = $aliasedFieldNames; |
| 196 | $sqlQuery->mOrigAliasedFieldNames = $aliasedFieldNames; |
| 197 | // Set mFieldsStr in case we need to show a "More" link |
| 198 | // at the end. |
| 199 | $fieldsStr = ''; |
| 200 | foreach ( $aliasedFieldNames as $alias => $fieldName ) { |
| 201 | $fieldsStr .= "$fieldName=$alias,"; |
| 202 | } |
| 203 | // Remove the comma at the end. |
| 204 | $sqlQuery->mFieldsStr = trim( $fieldsStr, ',' ); |
| 205 | |
| 206 | $sqlQuery->setDescriptionsAndTableNamesForFields(); |
| 207 | $sqlQuery->handleDateFields(); |
| 208 | $sqlQuery->setOrderBy(); |
| 209 | $sqlQuery->mQueryLimit = 100; |
| 210 | |
| 211 | $queryResults = $sqlQuery->run(); |
| 212 | |
| 213 | $displayParams = []; |
| 214 | $displayParams['max display chars'] = 300; |
| 215 | $displayParams['edit link'] = 'yes'; |
| 216 | |
| 217 | $queryDisplayer = CargoQueryDisplayer::newFromSQLQuery( $sqlQuery, $lang ); |
| 218 | $queryDisplayer->mDisplayParams = $displayParams; |
| 219 | $formattedQueryResults = $queryDisplayer->getFormattedQueryResults( $queryResults ); |
| 220 | |
| 221 | $tableFormat = new CargoTableFormat( $this->getOutput() ); |
| 222 | $text = |
| 223 | $tableFormat->display( $queryResults, $formattedQueryResults, |
| 224 | $sqlQuery->mFieldDescriptions, $displayParams ); |
| 225 | |
| 226 | // If there are (seemingly) more results than what we showed, |
| 227 | // show a "View more" link that links to Special:ViewData. |
| 228 | if ( count( $queryResults ) == $sqlQuery->mQueryLimit ) { |
| 229 | $text .= $queryDisplayer->viewMoreResultsLink(); |
| 230 | } |
| 231 | |
| 232 | $out->addHTML( $text ); |
| 233 | } |
| 234 | |
| 235 | public function displayNumRowsForTable( $cdb, $tableName ) { |
| 236 | global $wgCargoDecimalMark; |
| 237 | global $wgCargoDigitGroupingCharacter; |
| 238 | |
| 239 | $res = $cdb->select( $tableName, 'COUNT(*) AS total', '', __METHOD__ ); |
| 240 | $row = $res->fetchRow(); |
| 241 | |
| 242 | return number_format( intval( $row['total'] ), 0, $wgCargoDecimalMark, |
| 243 | $wgCargoDigitGroupingCharacter ); |
| 244 | } |
| 245 | |
| 246 | public function displayNumColumnsForTable( $tableName ) { |
| 247 | $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] ); |
| 248 | return (string)count( $tableSchemas[$tableName]->mFieldDescriptions ); |
| 249 | } |
| 250 | |
| 251 | private function getTableLinkedToView( $tableName, $isReplacementTable ) { |
| 252 | $viewURL = SpecialPage::getTitleFor( 'CargoTables' )->getFullURL() . "/$tableName"; |
| 253 | if ( $isReplacementTable ) { |
| 254 | $viewURL .= strpos( $viewURL, '?' ) ? '&' : '?'; |
| 255 | $viewURL .= "_replacement"; |
| 256 | } |
| 257 | |
| 258 | $displayText = |
| 259 | $isReplacementTable ? $this->msg( 'cargo-cargotables-replacementlink' ) : $tableName; |
| 260 | |
| 261 | return Html::element( 'a', [ 'href' => $viewURL ], $displayText ); |
| 262 | } |
| 263 | |
| 264 | public function getActionButton( $action, $target ) { |
| 265 | // a button is a clickable link, its target being a table action |
| 266 | $actionList = self::$actionList; |
| 267 | $displayIcon = $actionList[$action]['ooui-icon']; |
| 268 | $displayTitle = $this->msg( $actionList[$action]['ooui-title'] ); |
| 269 | $element = new OOUI\ButtonWidget( [ |
| 270 | 'icon' => $displayIcon, |
| 271 | 'title' => $displayTitle, |
| 272 | 'href' => $target, |
| 273 | ] ); |
| 274 | |
| 275 | return $element->toString(); |
| 276 | } |
| 277 | |
| 278 | private function getActionIcon( $action ) { |
| 279 | // an icon is just a static icon, no link. these are used in headings. |
| 280 | $actionList = self::$actionList; |
| 281 | $displayIcon = $actionList[$action]['ooui-icon']; |
| 282 | $displayTitle = $this->msg( $actionList[$action]['ooui-title'] ); |
| 283 | $element = new OOUI\IconWidget( [ |
| 284 | 'icon' => $displayIcon, |
| 285 | 'title' => $displayTitle, |
| 286 | ] ); |
| 287 | |
| 288 | return $element->toString(); |
| 289 | } |
| 290 | |
| 291 | private function setAllowedActions() { |
| 292 | // initialize needed ooui stuff |
| 293 | $this->getOutput()->enableOOUI(); |
| 294 | $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-interactions' ] ); |
| 295 | $this->getOutput()->addModuleStyles( [ 'oojs-ui.styles.icons-moderation' ] ); |
| 296 | OOUI\Element::setDefaultDir( 'ltr' ); |
| 297 | |
| 298 | // add display information for all actions that a user is able to perform |
| 299 | // if the parent param is set, then the action belongs to another column |
| 300 | // and will NOT cause creation of a new column |
| 301 | $user = $this->getUser(); |
| 302 | |
| 303 | $allowedActions = []; |
| 304 | if ( $user->isAllowed( 'runcargoqueries' ) ) { |
| 305 | $allowedActions['drilldown'] = [ |
| 306 | "ooui-icon" => "funnel", |
| 307 | "ooui-title" => "cargo-cargotables-action-drilldown", |
| 308 | ]; |
| 309 | } |
| 310 | |
| 311 | // recreatecargodata allows both recreating and switching in replacements |
| 312 | if ( $user->isAllowed( 'recreatecargodata' ) ) { |
| 313 | $allowedActions['recreate'] = |
| 314 | [ "ooui-icon" => "reload", "ooui-title" => "cargo-cargotables-action-recreate" ]; |
| 315 | $allowedActions['switchReplacement'] = |
| 316 | [ |
| 317 | "ooui-icon" => "check", |
| 318 | "ooui-title" => "cargo-cargotables-action-switchreplacement", |
| 319 | "parent" => "recreate", |
| 320 | ]; |
| 321 | // Should this have a separate permission? |
| 322 | $allowedActions['create'] = |
| 323 | [ |
| 324 | "ooui-icon" => "add", |
| 325 | "ooui-title" => "create", |
| 326 | "parent" => "recreate", |
| 327 | ]; |
| 328 | } |
| 329 | |
| 330 | // deletecargodata allows deleting live tables & their replacements |
| 331 | // these cases are handled separately so they can use separate icons |
| 332 | if ( $user->isAllowed( 'deletecargodata' ) ) { |
| 333 | $allowedActions['delete'] = |
| 334 | [ "ooui-icon" => "trash", "ooui-title" => "cargo-cargotables-action-delete" ]; |
| 335 | $allowedActions['deleteReplacement'] = |
| 336 | [ |
| 337 | "ooui-icon" => "cancel", |
| 338 | "ooui-title" => "cargo-cargotables-action-deletereplacement", |
| 339 | "parent" => "delete", |
| 340 | ]; |
| 341 | } |
| 342 | |
| 343 | // allow opportunity for adding additional actions & display info |
| 344 | $this->getHookContainer()->run( 'CargoTablesSetAllowedActions', [ $this, &$allowedActions ] ); |
| 345 | self::$actionList = $allowedActions; |
| 346 | } |
| 347 | |
| 348 | private function deriveListOfColumnsFromUserAllowedActions() { |
| 349 | $columns = []; |
| 350 | foreach ( self::$actionList as $action => $actionInfo ) { |
| 351 | if ( array_key_exists( "parent", $actionInfo ) ) { |
| 352 | continue; |
| 353 | } |
| 354 | $columns[] = $action; |
| 355 | } |
| 356 | |
| 357 | return $columns; |
| 358 | } |
| 359 | |
| 360 | private function getActionLinksForTable( $tableName, $isReplacementTable, $hasReplacementTable, $isSpecialTable = false ) { |
| 361 | $user = $this->getUser(); |
| 362 | |
| 363 | $canBeRecreated = |
| 364 | !$isReplacementTable && !$hasReplacementTable && |
| 365 | ( $isSpecialTable || array_key_exists( $tableName, $this->templatesThatDeclareTables ) ); |
| 366 | |
| 367 | $actionLinks = []; |
| 368 | |
| 369 | if ( array_key_exists( 'drilldown', self::$actionList ) ) { |
| 370 | $drilldownPage = CargoUtils::getSpecialPage( 'Drilldown' ); |
| 371 | $drilldownURL = $drilldownPage->getPageTitle()->getLocalURL() . '/' . $tableName; |
| 372 | $drilldownURL .= strpos( $drilldownURL, '?' ) ? '&' : '?'; |
| 373 | if ( $isReplacementTable ) { |
| 374 | $drilldownURL .= "_replacement"; |
| 375 | } else { |
| 376 | $drilldownURL .= "_single"; |
| 377 | } |
| 378 | $actionLinks['drilldown'] = $this->getActionButton( 'drilldown', $drilldownURL ); |
| 379 | } |
| 380 | |
| 381 | // Recreate permission governs both recreating and switching |
| 382 | if ( array_key_exists( 'recreate', self::$actionList ) ) { |
| 383 | // It's a bit odd to include the "Recreate data" link, since |
| 384 | // it's an action for the template and not the table (if a |
| 385 | // template defines two tables, this will recreate both of |
| 386 | // them), but for standard setups, this makes things more |
| 387 | // convenient. |
| 388 | if ( $canBeRecreated ) { |
| 389 | if ( $isSpecialTable ) { |
| 390 | $recreateURL = |
| 391 | SpecialPage::getTitleFor( 'RecreateCargoData' )->getFullURL() . "/$tableName"; |
| 392 | $actionLinks['recreate'] = $this->getActionButton( 'recreate', $recreateURL ); |
| 393 | } else { |
| 394 | $templateID = $this->templatesThatDeclareTables[$tableName][0]; |
| 395 | $templateTitle = Title::newFromID( $templateID ); |
| 396 | if ( $templateTitle !== null ) { |
| 397 | $recreateDataURL = $templateTitle->getLocalURL( [ 'action' => 'recreatedata' ] ); |
| 398 | $actionLinks['recreate'] = $this->getActionButton( 'recreate', $recreateDataURL ); |
| 399 | } |
| 400 | } |
| 401 | } elseif ( $isReplacementTable ) { |
| 402 | // switch will be in the same column as recreate |
| 403 | $switchURL = |
| 404 | SpecialPage::getTitleFor( 'SwitchCargoTable' )->getFullURL() . "/$tableName"; |
| 405 | $actionLinks['recreate'] = |
| 406 | $this->getActionButton( 'switchReplacement', $switchURL ); |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | if ( array_key_exists( 'delete', self::$actionList ) ) { |
| 411 | $deleteTableURL = |
| 412 | SpecialPage::getTitleFor( 'DeleteCargoTable' )->getLocalURL() . "/$tableName"; |
| 413 | $deleteAction = "delete"; |
| 414 | if ( $isReplacementTable ) { |
| 415 | $deleteTableURL .= strpos( $deleteTableURL, '?' ) ? '&' : '?'; |
| 416 | $deleteTableURL .= "_replacement"; |
| 417 | $deleteAction = "deleteReplacement"; |
| 418 | } |
| 419 | $actionLinks['delete'] = $this->getActionButton( $deleteAction, $deleteTableURL ); |
| 420 | } |
| 421 | |
| 422 | $this->getHookContainer()->run( 'CargoTablesSetActionLinks', [ |
| 423 | $this, |
| 424 | &$actionLinks, |
| 425 | $tableName, |
| 426 | $isReplacementTable, |
| 427 | $hasReplacementTable, |
| 428 | $this->templatesThatDeclareTables, |
| 429 | $this->templatesThatAttachToTables, |
| 430 | self::$actionList, |
| 431 | $user |
| 432 | ] ); |
| 433 | |
| 434 | return $actionLinks; |
| 435 | } |
| 436 | |
| 437 | private function tableTemplatesText( $tableName ) { |
| 438 | $linkRenderer = $this->getLinkRenderer(); |
| 439 | |
| 440 | // "Declared by" text |
| 441 | if ( !array_key_exists( $tableName, $this->templatesThatDeclareTables ) ) { |
| 442 | $declaringTemplatesText = $this->msg( 'cargo-cargotables-notdeclared' )->text(); |
| 443 | } else { |
| 444 | $templatesThatDeclareThisTable = $this->templatesThatDeclareTables[$tableName]; |
| 445 | $templateLinks = []; |
| 446 | foreach ( $templatesThatDeclareThisTable as $templateID ) { |
| 447 | $templateTitle = Title::newFromID( $templateID ); |
| 448 | $templateLinks[] = CargoUtils::makeLink( $linkRenderer, $templateTitle ); |
| 449 | } |
| 450 | $declaringTemplatesText = |
| 451 | Html::rawElement( 'span', [ "class" => "cargo-tablelist-template-declaring" ], |
| 452 | implode( ', ', $templateLinks ) ); |
| 453 | } |
| 454 | |
| 455 | // "Attached by" text |
| 456 | if ( array_key_exists( $tableName, $this->templatesThatAttachToTables ) ) { |
| 457 | $templatesThatAttachToThisTable = $this->templatesThatAttachToTables[$tableName]; |
| 458 | } else { |
| 459 | $templatesThatAttachToThisTable = []; |
| 460 | } |
| 461 | |
| 462 | if ( count( $templatesThatAttachToThisTable ) == 0 ) { |
| 463 | return $declaringTemplatesText; |
| 464 | } |
| 465 | |
| 466 | $templateLinks = []; |
| 467 | foreach ( $templatesThatAttachToThisTable as $templateID ) { |
| 468 | $templateTitle = Title::newFromID( $templateID ); |
| 469 | $templateLinks[] = CargoUtils::makeLink( $linkRenderer, $templateTitle ); |
| 470 | } |
| 471 | $attachingTemplatesText = |
| 472 | Html::rawElement( 'span', [ "class" => "cargo-tablelist-template-attaching" ], |
| 473 | implode( ', ', $templateLinks ) ); |
| 474 | |
| 475 | return "$declaringTemplatesText, $attachingTemplatesText"; |
| 476 | } |
| 477 | |
| 478 | /** |
| 479 | * Returns HTML for a bulleted list of Cargo tables, with various |
| 480 | * links and information for each one. |
| 481 | */ |
| 482 | private function displayListOfTables() { |
| 483 | global $wgCargoTablesPrioritizeReplacements; |
| 484 | $text = ''; |
| 485 | |
| 486 | $this->setAllowedActions(); |
| 487 | |
| 488 | $listOfColumns = $this->deriveListOfColumnsFromUserAllowedActions(); |
| 489 | |
| 490 | // Show a note if there are currently Cargo populate-data jobs |
| 491 | // that haven't been run, to make troubleshooting easier. |
| 492 | $group = MediaWikiServices::getInstance()->getJobQueueGroup(); |
| 493 | // The following line would have made more sense to call, but |
| 494 | // it seems to return true if there are *any* jobs in the |
| 495 | // queue - a bug in MediaWiki? |
| 496 | // if ( $group->queuesHaveJobs( 'cargoPopulateTable' ) ) { |
| 497 | if ( in_array( 'cargoPopulateTable', $group->getQueuesWithJobs() ) ) { |
| 498 | $text .= Html::warningBox( |
| 499 | $this->msg( 'cargo-cargotables-beingpopulated' )->parse() |
| 500 | ); |
| 501 | } |
| 502 | |
| 503 | $cdb = CargoUtils::getDB(); |
| 504 | $tableNames = CargoUtils::getTables(); |
| 505 | |
| 506 | // Move the "special" tables into a separate array. |
| 507 | $existingSpecialTables = []; |
| 508 | foreach ( $tableNames as $tableIndex => $tableName ) { |
| 509 | if ( substr( $tableName, 0, 1 ) === '_' ) { |
| 510 | unset( $tableNames[$tableIndex] ); |
| 511 | $existingSpecialTables[] = $tableName; |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | // reorder table list so tables with replacements are first, |
| 516 | // but only if the preference is set to do so |
| 517 | if ( $wgCargoTablesPrioritizeReplacements ) { |
| 518 | foreach ( $tableNames as $tableIndex => $tableName ) { |
| 519 | $possibleReplacementTable = $tableName . '__NEXT'; |
| 520 | if ( $cdb->tableExists( $possibleReplacementTable, __METHOD__ ) ) { |
| 521 | unset( $tableNames[$tableIndex] ); |
| 522 | array_unshift( $tableNames, $tableName ); |
| 523 | } |
| 524 | } |
| 525 | } |
| 526 | |
| 527 | $text .= Html::rawElement( 'p', [], $this->msg( 'cargo-cargotables-tablelist' ) |
| 528 | ->numParams( count( $tableNames ) ) |
| 529 | ->escaped() ) . "\n"; |
| 530 | |
| 531 | $headerText = Html::element( 'th', [], $this->msg( "cargo-cargotables-header-table" )->text() ); |
| 532 | $headerText .= Html::element( 'th', [], |
| 533 | $this->msg( "cargo-cargotables-header-rowcount" )->text() ); |
| 534 | $headerText .= Html::element( 'th', [ 'class' => 'cargotables-columncount' ], |
| 535 | $this->msg( "cargo-cargotables-header-columncount" )->text() ); |
| 536 | |
| 537 | foreach ( $listOfColumns as $action ) { |
| 538 | $headerText .= Html::rawElement( 'th', [], $this->getActionIcon( $action ) ); |
| 539 | } |
| 540 | |
| 541 | $headerText .= Html::element( 'th', [], |
| 542 | $this->msg( "cargo-cargotables-header-templates" )->text() ); |
| 543 | |
| 544 | $wikitableText = Html::rawElement( 'tr', [], $headerText ); |
| 545 | |
| 546 | foreach ( $tableNames as $tableName ) { |
| 547 | |
| 548 | $tableLink = $this->getTableLinkedToView( $tableName, false ); |
| 549 | |
| 550 | $rowText = ""; |
| 551 | if ( !CargoUtils::tableFullyExists( $tableName ) ) { |
| 552 | continue; |
| 553 | } |
| 554 | |
| 555 | $possibleReplacementTable = $tableName . '__NEXT'; |
| 556 | $hasReplacementTable = CargoUtils::tableFullyExists( $possibleReplacementTable ); |
| 557 | $actionLinks = $this->getActionLinksForTable( $tableName, false, $hasReplacementTable ); |
| 558 | |
| 559 | $numRowsText = $this->displayNumRowsForTable( $cdb, $tableName ); |
| 560 | $numColumnsText = $this->displayNumColumnsForTable( $tableName ); |
| 561 | $templatesText = $this->tableTemplatesText( $tableName ); |
| 562 | |
| 563 | $rowText .= Html::rawElement( 'td', [ 'class' => 'cargo-tablelist-tablename' ], |
| 564 | $tableLink ); |
| 565 | $rowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numrows' ], |
| 566 | $numRowsText ); |
| 567 | $rowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numcolumns' ], |
| 568 | $numColumnsText ); |
| 569 | |
| 570 | $this->displayActionLinks( $listOfColumns, $actionLinks, $rowText ); |
| 571 | |
| 572 | if ( !$hasReplacementTable ) { |
| 573 | $rowText .= Html::rawElement( 'td', [], $templatesText ); |
| 574 | $wikitableText .= Html::rawElement( 'tr', [], $rowText ); |
| 575 | continue; |
| 576 | } |
| 577 | |
| 578 | // if there's a replacement table, the template links need to span 2 rows |
| 579 | $rowText .= Html::rawElement( 'td', [ 'rowspan' => 2 ], $templatesText ); |
| 580 | $wikitableText .= Html::rawElement( 'tr', [], $rowText ); |
| 581 | |
| 582 | $replacementRowText = ''; |
| 583 | $tableLink = $this->getTableLinkedToView( $tableName, true ); |
| 584 | |
| 585 | $numRowsText = $this->displayNumRowsForTable( $cdb, $tableName . '__NEXT' ); |
| 586 | $numColumnsText = $this->displayNumColumnsForTable( $tableName . '__NEXT' ); |
| 587 | $actionLinks = $this->getActionLinksForTable( $tableName, true, false ); |
| 588 | |
| 589 | $replacementRowText .= Html::rawElement( 'td', |
| 590 | [ 'class' => 'cargo-tablelist-tablename' ], $tableLink ); |
| 591 | $replacementRowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numrows' ], |
| 592 | $numRowsText ); |
| 593 | $replacementRowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numcolumns' ], |
| 594 | $numColumnsText ); |
| 595 | |
| 596 | $this->displayActionLinks( $listOfColumns, $actionLinks, $replacementRowText ); |
| 597 | |
| 598 | $wikitableText .= Html::rawElement( 'tr', |
| 599 | [ 'class' => 'cargo-tablelist-replacement-row' ], $replacementRowText ); |
| 600 | } |
| 601 | $text .= Html::rawElement( 'table', [ 'class' => 'mw-datatable cargo-tablelist' ], |
| 602 | $wikitableText ); |
| 603 | |
| 604 | // Now display the table for the special Cargo tables. |
| 605 | $text .= '<br />'; |
| 606 | $text .= Html::element( 'p', [], $this->msg( 'cargo-cargotables-specialtables' )->escaped() ); |
| 607 | $specialTableNames = CargoUtils::specialTableNames(); |
| 608 | $headerText = Html::element( 'th', [], $this->msg( "cargo-cargotables-header-table" )->escaped() ); |
| 609 | $headerText .= Html::element( 'th', [], |
| 610 | $this->msg( "cargo-cargotables-header-rowcount" )->escaped() ); |
| 611 | $headerText .= Html::element( 'th', [ 'class' => 'cargotables-columncount' ], |
| 612 | $this->msg( "cargo-cargotables-header-columncount" )->escaped() ); |
| 613 | |
| 614 | $invalidActionsForSpecialTables = [ 'edit' ]; |
| 615 | foreach ( $listOfColumns as $i => $action ) { |
| 616 | if ( in_array( $action, $invalidActionsForSpecialTables ) ) { |
| 617 | unset( $listOfColumns[$i] ); |
| 618 | continue; |
| 619 | } |
| 620 | $headerText .= Html::rawElement( 'th', [], $this->getActionIcon( $action ) ); |
| 621 | } |
| 622 | $wikitableText = Html::rawElement( 'tr', [], $headerText ); |
| 623 | |
| 624 | foreach ( $specialTableNames as $specialTableName ) { |
| 625 | $rowText = ''; |
| 626 | $tableExists = in_array( $specialTableName, $existingSpecialTables ); |
| 627 | if ( $tableExists ) { |
| 628 | $possibleReplacementTable = $specialTableName . '__NEXT'; |
| 629 | $hasReplacementTable = CargoUtils::tableFullyExists( $possibleReplacementTable ); |
| 630 | $actionLinks = $this->getActionLinksForTable( $specialTableName, false, $hasReplacementTable, true ); |
| 631 | } else { |
| 632 | $hasReplacementTable = false; |
| 633 | $actionLinks = []; |
| 634 | if ( array_key_exists( 'recreate', self::$actionList ) ) { |
| 635 | $recreateURL = |
| 636 | SpecialPage::getTitleFor( 'RecreateCargoData' )->getFullURL() . "/$specialTableName"; |
| 637 | $actionLinks['recreate'] = $this->getActionButton( 'create', $recreateURL ); |
| 638 | } |
| 639 | } |
| 640 | foreach ( $actionLinks as $action => $actionLink ) { |
| 641 | if ( in_array( $action, $invalidActionsForSpecialTables ) ) { |
| 642 | unset( $actionLinks[$action] ); |
| 643 | } |
| 644 | } |
| 645 | if ( $tableExists ) { |
| 646 | $tableLink = $this->getTableLinkedToView( $specialTableName, false ); |
| 647 | } else { |
| 648 | $tableLink = $specialTableName; |
| 649 | } |
| 650 | $rowText .= Html::rawElement( 'td', [ 'class' => 'cargo-tablelist-tablename' ], |
| 651 | $tableLink ); |
| 652 | if ( $tableExists ) { |
| 653 | $numRowsText = $this->displayNumRowsForTable( $cdb, $specialTableName ); |
| 654 | $numColumnsText = $this->displayNumColumnsForTable( $specialTableName ); |
| 655 | } else { |
| 656 | $numRowsText = ''; |
| 657 | $numColumnsText = ''; |
| 658 | } |
| 659 | $rowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numrows' ], |
| 660 | $numRowsText ); |
| 661 | |
| 662 | $rowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numcolumns' ], |
| 663 | $numColumnsText ); |
| 664 | |
| 665 | $this->displayActionLinks( $listOfColumns, $actionLinks, $rowText ); |
| 666 | $wikitableText .= Html::rawElement( 'tr', [], $rowText ); |
| 667 | |
| 668 | if ( !$hasReplacementTable ) { |
| 669 | continue; |
| 670 | } |
| 671 | |
| 672 | // if there's a replacement table, the template links need to span 2 rows |
| 673 | $replacementRowText = ''; |
| 674 | $tableLink = $this->getTableLinkedToView( $specialTableName, true ); |
| 675 | |
| 676 | $numRowsText = $this->displayNumRowsForTable( $cdb, $specialTableName . '__NEXT' ); |
| 677 | $numColumnsText = $this->displayNumColumnsForTable( $specialTableName . '__NEXT' ); |
| 678 | $actionLinks = $this->getActionLinksForTable( $specialTableName, true, false ); |
| 679 | |
| 680 | $replacementRowText .= Html::rawElement( 'td', |
| 681 | [ 'class' => 'cargo-tablelist-tablename' ], $tableLink ); |
| 682 | $replacementRowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numrows' ], |
| 683 | $numRowsText ); |
| 684 | $replacementRowText .= Html::element( 'td', [ 'class' => 'cargo-tablelist-numcolumns' ], |
| 685 | $numColumnsText ); |
| 686 | |
| 687 | $this->displayActionLinks( $listOfColumns, $actionLinks, $replacementRowText ); |
| 688 | |
| 689 | $wikitableText .= Html::rawElement( 'tr', |
| 690 | [ 'class' => 'cargo-tablelist-replacement-row' ], $replacementRowText ); |
| 691 | } |
| 692 | |
| 693 | $text .= Html::rawElement( 'table', [ |
| 694 | 'class' => 'mw-datatable cargo-tablelist', |
| 695 | 'style' => 'min-width: auto;' |
| 696 | ], |
| 697 | $wikitableText ); |
| 698 | |
| 699 | return $text; |
| 700 | } |
| 701 | |
| 702 | private function displayActionLinks( $listOfColumns, $actionLinks, &$rowText ) { |
| 703 | foreach ( $listOfColumns as $action ) { |
| 704 | if ( array_key_exists( $action, $actionLinks ) ) { |
| 705 | $rowText .= Html::rawElement( 'td', [ "class" => "cargo-tablelist-actionbutton" ], |
| 706 | $actionLinks[$action] ); |
| 707 | } else { |
| 708 | $rowText .= Html::rawElement( 'td', [], '' ); |
| 709 | } |
| 710 | } |
| 711 | } |
| 712 | |
| 713 | protected function getGroupName() { |
| 714 | return 'cargo'; |
| 715 | } |
| 716 | } |