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