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