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