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