Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CargoPageValues
0.00% covered (danger)
0.00%
0 / 170
0.00% covered (danger)
0.00%
0 / 12
2652
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
420
 getTableLink
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getInfoForAllFields
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getRowsForPageInTable
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 printRow
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 printTable
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 tocIndent
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tocLine
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 tocLineEnd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tocList
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 isListed
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Displays an interface to let users recreate data via the Cargo
4 * extension.
5 *
6 * @author Yaron Koren
7 * @ingroup Cargo
8 */
9
10use MediaWiki\Html\Html;
11use MediaWiki\Title\Title;
12
13class CargoPageValues extends IncludableSpecialPage {
14    public $mTitle;
15
16    public function __construct( $title = null ) {
17        parent::__construct( 'PageValues' );
18
19        $this->mTitle = $title;
20    }
21
22    public function execute( $subpage = null ) {
23        if ( $subpage ) {
24            // Allow inclusion with e.g. {{Special:PageValues/Book}}
25            $this->mTitle = Title::newFromText( $subpage );
26        }
27
28        // If no title, or a nonexistent title, was set, just exit out.
29        // @TODO - display an error message.
30        if ( $this->mTitle == null || !$this->mTitle->exists() ) {
31            return true;
32        }
33
34        $out = $this->getOutput();
35
36        $this->setHeaders();
37
38        $pageName = $this->mTitle->getPrefixedText();
39        $out->setPageTitle( $this->msg( 'cargo-pagevaluesfor', $pageName )->text() );
40
41        $text = '';
42
43        $tableNames = [];
44
45        $cdb = CargoUtils::getDB();
46        if ( $cdb->tableExists( '_pageData__NEXT', __METHOD__ ) ) {
47            $tableNames[] = '_pageData__NEXT';
48        } elseif ( $cdb->tableExists( '_pageData', __METHOD__ ) ) {
49            $tableNames[] = '_pageData';
50        }
51        if ( $cdb->tableExists( '_fileData__NEXT', __METHOD__ ) ) {
52            $tableNames[] = '_fileData__NEXT';
53        } elseif ( $cdb->tableExists( '_fileData', __METHOD__ ) ) {
54            $tableNames[] = '_fileData';
55        }
56
57        $dbr = CargoUtils::getMainDBForRead();
58        $res = $dbr->select(
59            'cargo_pages', 'table_name',
60            [ 'page_id' => $this->mTitle->getArticleID() ],
61            __METHOD__
62        );
63        foreach ( $res as $row ) {
64            $tableNames[] = $row->table_name;
65        }
66
67        $toc = self::tocIndent();
68        $tocLength = 0;
69
70        foreach ( $tableNames as $tableName ) {
71            try {
72                $queryResults = $this->getRowsForPageInTable( $tableName );
73            } catch ( Exception $e ) {
74                // Most likely this is because the _pageData
75                // table doesn't exist.
76                continue;
77            }
78            $numRowsOnPage = count( $queryResults );
79
80            // Hide _fileData if it's empty - we do this only for _fileData,
81            // as another table having 0 rows can indicate an error, and we'd
82            // like to preserve that information for debugging purposes.
83            if ( $numRowsOnPage === 0 && ( $tableName === '_fileData' || $tableName === '_fileData__NEXT' ) ) {
84                continue;
85            }
86
87            $tableLink = $this->getTableLink( $tableName );
88
89            $tableSectionHeader = $this->msg( 'cargo-pagevalues-tablevalues' )->rawParams( $tableLink )->escaped();
90            $tableSectionTocDisplay = $this->msg( 'cargo-pagevalues-tablevalues', $tableName )->escaped();
91            $tableSectionAnchor = $this->msg( 'cargo-pagevalues-tablevalues', $tableName )->escaped();
92            $tableSectionAnchor = Sanitizer::escapeIdForAttribute( $tableSectionAnchor );
93
94            // We construct the table of contents at the same time
95            // as the main text.
96            $toc .= self::tocLine( $tableSectionAnchor, $tableSectionTocDisplay,
97                $this->getLanguage()->formatNum( ++$tocLength ), 1 ) . self::tocLineEnd();
98
99            $h2 = Html::rawElement( 'h2', null,
100                Html::rawElement( 'span', [ 'class' => 'mw-headline', 'id' => $tableSectionAnchor ], $tableSectionHeader ) );
101
102            $text .= Html::rawElement( 'div', [ 'class' => 'cargo-pagevalues-tableinfo' ],
103                $h2 . $this->msg( "cargo-pagevalues-tableinfo-numrows", $numRowsOnPage )
104            );
105
106            foreach ( $queryResults as $rowValues ) {
107                $tableContents = '';
108                $fieldInfo = $this->getInfoForAllFields( $tableName );
109                $anyFieldHasAllowedValues = false;
110                foreach ( $fieldInfo as $info ) {
111                    if ( $info['allowed values'] !== '' ) {
112                        $anyFieldHasAllowedValues = true;
113                    }
114                }
115                foreach ( $rowValues as $field => $value ) {
116                    // @HACK - this check should ideally
117                    // be done earlier.
118                    if ( strpos( $field, '__precision' ) !== false ) {
119                        continue;
120                    }
121                    $tableContents .= $this->printRow( $field, $value, $fieldInfo[$field], $anyFieldHasAllowedValues );
122                }
123                $text .= $this->printTable( $tableContents, $anyFieldHasAllowedValues );
124            }
125        }
126
127        // Show table of contents only if there are enough sections.
128        if ( count( $tableNames ) >= 3 ) {
129            $toc = self::tocList( $toc );
130            $out->addHTML( $toc );
131        }
132
133        $out->addHTML( $text );
134        $out->addModules( 'ext.cargo.main' );
135        $out->addModuleStyles( 'ext.cargo.pagevalues' );
136
137        return true;
138    }
139
140    private function getTableLink( $tableName ) {
141        $originalTableName = str_replace( '__NEXT', '', $tableName );
142        $isReplacementTable = substr( $tableName, -6 ) == '__NEXT';
143        $viewURL = SpecialPage::getTitleFor( 'CargoTables' )->getFullURL() . "/$originalTableName";
144        if ( $isReplacementTable ) {
145            $viewURL .= strpos( $viewURL, '?' ) ? '&' : '?';
146            $viewURL .= "_replacement";
147        }
148
149        return Html::element( 'a', [ 'href' => $viewURL ], $tableName );
150    }
151
152    /**
153     * Used to get the information about field type and the list
154     * of allowed values (if any) of all fields of a table.
155     *
156     * @param string $tableName
157     */
158    private function getInfoForAllFields( $tableName ) {
159        $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] );
160        if ( $tableName == '_pageData' || $tableName == '_pageData__NEXT' ) {
161            CargoUtils::addGlobalFieldsToSchema( $tableSchemas[$tableName] );
162        }
163        $fieldDescriptions = $tableSchemas[$tableName]->mFieldDescriptions;
164        $fieldInfo = [];
165        foreach ( $fieldDescriptions as $fieldName => $fieldDescription ) {
166            $fieldInfo[$fieldName]['field type'] = $fieldDescription->prettyPrintType();
167            if ( is_array( $fieldDescription->mAllowedValues ) ) {
168                $fieldInfo[$fieldName]['allowed values'] = $fieldDescription->prettyPrintAllowedValues();
169            } else {
170                $fieldInfo[$fieldName]['allowed values'] = '';
171            }
172        }
173        return $fieldInfo;
174    }
175
176    public function getRowsForPageInTable( $tableName ) {
177        $cdb = CargoUtils::getDB();
178
179        $sqlQuery = new CargoSQLQuery();
180        $sqlQuery->mAliasedTableNames = [ $tableName => $tableName ];
181
182        $tableSchemas = CargoUtils::getTableSchemas( [ $tableName ] );
183
184        if ( $tableName == '_pageData' || $tableName == '_pageData__NEXT' ) {
185            CargoUtils::addGlobalFieldsToSchema( $tableSchemas[$tableName] );
186        }
187
188        $sqlQuery->mTableSchemas = $tableSchemas;
189
190        $aliasedFieldNames = [];
191        foreach ( $tableSchemas[$tableName]->mFieldDescriptions as $fieldName => $fieldDescription ) {
192            if ( $fieldDescription->mIsHidden ) {
193                // @TODO - do some custom formatting
194            }
195
196            // $fieldAlias = str_replace( '_', ' ', $fieldName );
197            $fieldAlias = $fieldName;
198
199            if ( $fieldDescription->mIsList ) {
200                $aliasedFieldNames[$fieldAlias] = $fieldName . '__full';
201            } elseif ( $fieldDescription->mType == 'Coordinates' ) {
202                $aliasedFieldNames[$fieldAlias] = $fieldName . '__full';
203            } else {
204                $aliasedFieldNames[$fieldAlias] = $fieldName;
205            }
206        }
207
208        $sqlQuery->mAliasedFieldNames = $aliasedFieldNames;
209        $sqlQuery->mOrigAliasedFieldNames = $aliasedFieldNames;
210        $sqlQuery->setDescriptionsAndTableNamesForFields();
211        $sqlQuery->handleDateFields();
212        $sqlQuery->mWhereStr = $cdb->addIdentifierQuotes( '_pageID' ) . " = " .
213            $this->mTitle->getArticleID();
214
215        $queryResults = $sqlQuery->run();
216        $queryDisplayer = CargoQueryDisplayer::newFromSQLQuery( $sqlQuery );
217        $formattedQueryResults = $queryDisplayer->getFormattedQueryResults( $queryResults );
218        return $formattedQueryResults;
219    }
220
221    /**
222     * Based on MediaWiki's InfoAction::addRow()
223     */
224    public function printRow( $name, $value, $fieldInfo, $fieldHasAnyAllowedValues ) {
225        if ( $name == '_fullText' && strlen( $value ) > 300 ) {
226            $value = substr( $value, 0, 300 ) . ' ...';
227        }
228        $text = Html::element( 'td', [ 'class' => 'cargo-pagevalues-table-field' ], $name ) .
229            Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-type' ], $fieldInfo['field type'] );
230        if ( $fieldHasAnyAllowedValues ) {
231            $allowedValuesText = $fieldInfo['allowed values'];
232            // Count "middot" as only one character, not eight, when counting the string length.
233            $allowedValuesDisplayText = str_replace( '&middot;', '.', $allowedValuesText );
234            if ( strlen( $allowedValuesDisplayText ) > 25 ) {
235                $allowedValuesText = '<span class="cargoMinimizedText">' . $fieldInfo['allowed values'] . '</span>';
236            }
237            $text .= Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-allowedvalues' ], $allowedValuesText );
238        }
239        $text .= Html::rawElement( 'td', [ 'class' => 'cargo-pagevalues-table-value' ], $value );
240
241        return Html::rawElement( 'tr', [], $text );
242    }
243
244    /**
245     * Based on MediaWiki's InfoAction::addTable()
246     */
247    public function printTable( $tableContents, $anyFieldHasAllowedValues ) {
248        $headerRow = Html::element( 'th', null, $this->msg( 'cargo-field' )->text() ) .
249            Html::element( 'th', null, $this->msg( 'cargo-field-type' )->text() );
250        if ( $anyFieldHasAllowedValues ) {
251            $headerRow .= Html::element( 'th', null, $this->msg( 'cargo-allowed-values' )->text() );
252        }
253        $headerRow .= Html::element( 'th', null, $this->msg( 'cargo-value' )->text() );
254        $headerRow = Html::rawElement( 'tr', null, $headerRow );
255        return Html::rawElement( 'table', [ 'class' => 'wikitable mw-page-info' ],
256            $headerRow . $tableContents ) . "\n";
257    }
258
259    /**
260     * Add another level to the Table of Contents
261     *
262     * Copied from HandleTOCMarkers::tocIndent(), which is unfortunately private.
263     *
264     * @return string
265     */
266    private static function tocIndent() {
267        return "\n<ul>\n";
268    }
269
270    /**
271     * parameter level defines if we are on an indentation level
272     *
273     * Copied from HandleTOCMarkers::tocLine(), which is unfortunately private.
274     *
275     * @param string $linkAnchor Identifier
276     * @param string $tocline Properly escaped HTML
277     * @param string $tocnumber Unescaped text
278     * @param int $level
279     * @param string|false $sectionIndex
280     * @return string
281     */
282    private static function tocLine( $linkAnchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
283        $classes = "toclevel-$level";
284        // Parser.php used to suppress tocLine by setting $sectionindex to false.
285        // In those circumstances, we can now encounter '' or a "T-" prefixed index
286        // for when the section comes from templates.
287        if ( $sectionIndex !== false && $sectionIndex !== '' && !str_starts_with( $sectionIndex, "T-" ) ) {
288            $classes .= " tocsection-$sectionIndex";
289        }
290        // <li class="$classes"><a href="#$linkAnchor"><span class="tocnumber">
291        // $tocnumber</span> <span class="toctext">$tocline</span></a>
292        return Html::openElement( 'li', [ 'class' => $classes ] )
293            . Html::rawElement( 'a',
294                [ 'href' => "#$linkAnchor" ],
295                Html::element( 'span', [ 'class' => 'tocnumber' ], $tocnumber )
296                    . ' '
297                    . Html::rawElement( 'span', [ 'class' => 'toctext' ], $tocline )
298            );
299    }
300
301    /**
302     * End a Table Of Contents line.
303     * tocUnindent() will be used instead if we're ending a line below
304     * the new level.
305     *
306     * Copied from HandleTOCMarkers::tocLineEnd(), which is unfortunately private.
307     *
308     * @return string
309     */
310    private static function tocLineEnd() {
311        return "</li>\n";
312    }
313
314    /**
315     * Wraps the TOC in a div with ARIA navigation role and provides the hide/collapse JavaScript.
316     *
317     * Copied from HandleTOCMarkers::tocList(), which is unfortunately private.
318     *
319     * @param string $toc Html of the Table Of Contents
320     * @param Language|null $lang Language for the toc title, defaults to user language
321     * @return string Full html of the TOC
322     */
323    private static function tocList( $toc, ?Language $lang = null ) {
324        $lang ??= RequestContext::getMain()->getLanguage();
325        $title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
326        return '<div id="toc" class="toc" role="navigation" aria-labelledby="mw-toc-heading">'
327            . Html::element( 'input', [
328                'type' => 'checkbox',
329                'role' => 'button',
330                'id' => 'toctogglecheckbox',
331                'class' => 'toctogglecheckbox',
332                'style' => 'display:none',
333            ] )
334            . Html::openElement( 'div', [
335                'class' => 'toctitle',
336                'lang' => $lang->getHtmlCode(),
337                'dir' => $lang->getDir(),
338            ] )
339            . '<h2 id="mw-toc-heading">' . $title . '</h2>'
340            . '<span class="toctogglespan">'
341            . Html::label( '', 'toctogglecheckbox', [
342                'class' => 'toctogglelabel',
343            ] )
344            . '</span>'
345            . '</div>'
346            . $toc
347            . "</ul>\n</div>\n";
348    }
349
350    /**
351     * Don't list this in Special:SpecialPages.
352     */
353    public function isListed() {
354        return false;
355    }
356}