Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 428
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 / 428
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 / 129
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            $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}