Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 170 |
|
0.00% |
0 / 12 |
CRAP | |
0.00% |
0 / 1 |
CargoPageValues | |
0.00% |
0 / 170 |
|
0.00% |
0 / 12 |
2652 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 67 |
|
0.00% |
0 / 1 |
420 | |||
getTableLink | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getInfoForAllFields | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
getRowsForPageInTable | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
printRow | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
printTable | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
tocIndent | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tocLine | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
tocLineEnd | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tocList | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
2 | |||
isListed | |
0.00% |
0 / 1 |
|
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 | |
10 | use MediaWiki\Html\Html; |
11 | use MediaWiki\Title\Title; |
12 | |
13 | class 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( '·', '.', $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 | } |