Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
35.78% |
78 / 218 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
CargoDeclare | |
35.78% |
78 / 218 |
|
0.00% |
0 / 3 |
1647.35 | |
0.00% |
0 / 1 |
validateFieldOrTableName | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
11 | |||
run | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
5.07 | |||
declareTable | |
24.73% |
45 / 182 |
|
0.00% |
0 / 1 |
1648.11 |
1 | <?php |
2 | /** |
3 | * Class for the #cargo_declare parser function, as well as for the creation |
4 | * (and re-creation) of Cargo database tables. |
5 | * |
6 | * @author Yaron Koren |
7 | * @ingroup Cargo |
8 | */ |
9 | |
10 | use MediaWiki\MediaWikiServices; |
11 | |
12 | class CargoDeclare { |
13 | |
14 | public static $settings = []; |
15 | |
16 | /** |
17 | * "Reserved words" - terms that should not be used as table or field |
18 | * names, because they are reserved for SQL. |
19 | * (Some words here are much more likely to be used than others.) |
20 | * @TODO - currently this list only includes reserved words from |
21 | * MySQL; other DB systems' additional words (if any) should be added |
22 | * as well. |
23 | * |
24 | * @var string[] |
25 | */ |
26 | private static $sqlReservedWords = [ |
27 | 'accessible', 'add', 'all', 'alter', 'analyze', |
28 | 'and', 'as', 'asc', 'asensitive', 'before', |
29 | 'between', 'bigint', 'binary', 'blob', 'both', |
30 | 'by', 'call', 'cascade', 'case', 'change', |
31 | 'char', 'character', 'check', 'collate', 'column', |
32 | 'condition', 'constraint', 'continue', 'convert', 'create', |
33 | 'cross', 'current_date', 'current_time', 'current_timestamp', 'current_user', |
34 | 'cursor', 'database', 'databases', 'day_hour', 'day_microsecond', |
35 | 'day_minute', 'day_second', 'dec', 'decimal', 'declare', |
36 | 'default', 'delayed', 'delete', 'desc', 'describe', |
37 | 'deterministic', 'distinct', 'distinctrow', 'div', 'double', |
38 | 'drop', 'dual', 'each', 'else', 'elseif', |
39 | 'enclosed', 'escaped', 'exists', 'exit', 'explain', |
40 | 'false', 'fetch', 'float', 'float4', 'float8', |
41 | 'for', 'force', 'foreign', 'from', 'fulltext', |
42 | 'generated', 'get', 'grant', 'group', 'having', |
43 | 'high_priority', 'hour_microsecond', 'hour_minute', 'hour_second', 'if', |
44 | 'ignore', 'in', 'index', 'infile', 'inner', |
45 | 'inout', 'insensitive', 'insert', 'int', 'int1', |
46 | 'int2', 'int3', 'int4', 'int8', 'integer', |
47 | 'interval', 'into', 'io_after_gtids', 'io_before_gtids', 'is', |
48 | 'iterate', 'join', 'key', 'keys', 'kill', |
49 | 'leading', 'leave', 'left', 'like', 'limit', |
50 | 'linear', 'lines', 'load', 'localtime', 'localtimestamp', |
51 | 'lock', 'long', 'longblob', 'longtext', 'loop', |
52 | 'low_priority', 'master_bind', 'master_ssl_verify_server_cert', 'match', 'maxvalue', |
53 | 'mediumblob', 'mediumint', 'mediumtext', 'middleint', 'minute_microsecond', |
54 | 'minute_second', 'mod', 'modifies', 'natural', 'no_write_to_binlog', |
55 | 'not', 'null', 'numeric', 'on', 'optimize', |
56 | 'optimizer_costs', 'option', 'optionally', 'or', 'order', |
57 | 'out', 'outer', 'outfile', 'partition', 'precision', |
58 | 'primary', 'procedure', 'purge', 'range', 'read', |
59 | 'read_write', 'reads', 'real', 'references', 'regexp', |
60 | 'release', 'rename', 'repeat', 'replace', 'require', |
61 | 'resignal', 'restrict', 'return', 'revoke', 'right', |
62 | 'rlike', 'schema', 'schemas', 'second_microsecond', 'select', |
63 | 'sensitive', 'separator', 'set', 'show', 'signal', |
64 | 'smallint', 'spatial', 'specific', 'sql', 'sql_big_result', |
65 | 'sql_calc_found_rows', 'sql_small_result', 'sqlexception', 'sqlstate', 'sqlwarning', |
66 | 'ssl', 'starting', 'stored', 'straight_join', 'table', |
67 | 'terminated', 'then', 'tinyblob', 'tinyint', 'tinytext', |
68 | 'to', 'trailing', 'trigger', 'true', 'undo', |
69 | 'union', 'unique', 'unlock', 'unsigned', 'update', |
70 | 'usage', 'use', 'using', 'utc_date', 'utc_time', |
71 | 'utc_timestamp', 'values', 'varbinary', 'varchar', 'varcharacter', |
72 | 'varying', 'virtual', 'when', 'where', 'while', |
73 | 'with', 'write', 'xor', 'year_month', 'zerofill' |
74 | ]; |
75 | |
76 | /** |
77 | * Words that are similarly reserved for Cargo - thankfully, a much |
78 | * shorter list. |
79 | * |
80 | * @var string[] |
81 | */ |
82 | private static $cargoReservedWords = [ |
83 | 'holds', 'matches', 'near', 'within' |
84 | ]; |
85 | |
86 | /** |
87 | * FIXME: Should this throw an exception instead? This will |
88 | * enhance the method to be better tested. |
89 | * |
90 | * @param string $name The name of the table or field |
91 | * @param string $type The type of the field provided |
92 | * @return string|null |
93 | */ |
94 | public static function validateFieldOrTableName( $name, $type ) { |
95 | // We can just call text() on all of these wfMessage() calls, |
96 | // since the resulting text is passed to formatFieldError(), |
97 | // which HTML-encodes the text. |
98 | if ( $type == 'table' ) { |
99 | $typeDisplay = wfMessage( 'cargo-cargotables-header-table' )->text(); |
100 | } elseif ( $type == 'parent table' ) { |
101 | $typeDisplay = wfMessage( 'cargo-parenttable' )->text(); |
102 | } elseif ( $type == 'field' ) { |
103 | $typeDisplay = wfMessage( 'cargo-field' )->text(); |
104 | } else { |
105 | $typeDisplay = ''; |
106 | } |
107 | if ( preg_match( '/\s/', $name ) ) { |
108 | return wfMessage( "cargo-declare-validate-has-whitespace", $typeDisplay, $name )->text(); |
109 | } elseif ( strpos( $name, '_' ) === 0 ) { |
110 | return wfMessage( "cargo-declare-validate-starts-underscore", $typeDisplay, $name )->text(); |
111 | } elseif ( substr( $name, -1 ) === '_' ) { |
112 | return wfMessage( "cargo-declare-validate-ends-underscore", $typeDisplay, $name )->text(); |
113 | } elseif ( strpos( $name, '__' ) !== false ) { |
114 | return wfMessage( "cargo-declare-validate-gt1-underscore", $typeDisplay, $name )->text(); |
115 | } elseif ( preg_match( '/[\.,\-\'"<>(){}\[\]\\\\\/]/', $name ) ) { |
116 | return wfMessage( "cargo-declare-validate-bad-character", $typeDisplay, $name, '.,-\'"<>(){}[]\/' )->text(); |
117 | } elseif ( in_array( strtolower( $name ), self::$sqlReservedWords ) ) { |
118 | return wfMessage( "cargo-declare-validate-name-sql-kw", $name, $typeDisplay )->text(); |
119 | } elseif ( in_array( strtolower( $name ), self::$cargoReservedWords ) ) { |
120 | return wfMessage( "cargo-declare-validate-name-cargo-kw", $name, $typeDisplay )->text(); |
121 | } |
122 | return null; |
123 | } |
124 | |
125 | /** |
126 | * Handles the #cargo_declare parser function. |
127 | * |
128 | * @param Parser $parser |
129 | * @return string |
130 | */ |
131 | public static function run( Parser $parser ) { |
132 | if ( !$parser->getTitle() || $parser->getTitle()->getNamespace() != NS_TEMPLATE ) { |
133 | return CargoUtils::formatError( wfMessage( "cargo-declare-must-from-template" )->parse() ); |
134 | } |
135 | |
136 | $params = func_get_args(); |
137 | array_shift( $params ); // we already know the $parser... |
138 | $args = []; |
139 | foreach ( $params as $key => $value ) { |
140 | $parts = explode( '=', $value, 2 ); |
141 | if ( count( $parts ) != 2 ) { |
142 | continue; |
143 | } |
144 | $key = trim( $parts[0] ); |
145 | $value = trim( $parts[1] ); |
146 | $args[$key] = $value; |
147 | } |
148 | $text = self::declareTable( $parser, $args ); |
149 | return $text; |
150 | } |
151 | |
152 | /** |
153 | * Implements #cargo_declare functionality which is shared among parser function and lua |
154 | * |
155 | * @param Parser $parser |
156 | * @param array $params |
157 | * @return string|null |
158 | */ |
159 | public static function declareTable( $parser, $params ) { |
160 | $tableName = null; |
161 | $parentTables = []; |
162 | $drilldownTabsParams = []; |
163 | $tableSchema = new CargoTableSchema(); |
164 | $hasStartEvent = false; |
165 | $hasEndEvent = false; |
166 | foreach ( $params as $key => $value ) { |
167 | if ( $key == '_table' ) { |
168 | $tableName = $value; |
169 | if ( in_array( strtolower( $tableName ), self::$sqlReservedWords ) ) { |
170 | return CargoUtils::formatError( wfMessage( "cargo-declare-tablenm-is-sql-kw", $tableName )->parse() ); |
171 | } elseif ( in_array( strtolower( $tableName ), self::$cargoReservedWords ) ) { |
172 | return CargoUtils::formatError( wfMessage( "cargo-declare-tablenm-is-cargo-kw", $tableName )->parse() ); |
173 | } |
174 | } elseif ( $key == '_parentTables' ) { |
175 | $tables = explode( ';', $value ); |
176 | foreach ( $tables as $table ) { |
177 | $parentTable = []; |
178 | $parentTableAlias = ''; |
179 | $foundMatch = preg_match( '/([^(]*)\s*\((.*)\)/s', $table, $matches ); |
180 | if ( $foundMatch ) { |
181 | $parentTableName = trim( $matches[1] ); |
182 | if ( count( $matches ) >= 2 ) { |
183 | $extraParams = explode( ',', $matches[2] ); |
184 | foreach ( $extraParams as $extraParam ) { |
185 | if ( $extraParam ) { |
186 | $extraParamParts = explode( '=', $extraParam, 2 ); |
187 | $extraParamKey = trim( $extraParamParts[0] ); |
188 | $extraParamValue = trim( $extraParamParts[1] ); |
189 | if ( $extraParamKey == '_localField' ) { |
190 | $parentTable['_localField'] = $extraParamValue; |
191 | } elseif ( $extraParamKey == '_remoteField' ) { |
192 | $parentTable['_remoteField'] = $extraParamValue; |
193 | } elseif ( $extraParamKey == '_alias' ) { |
194 | $parentTableAlias = $extraParamValue; |
195 | } else { |
196 | // It shouldn't be anything else. |
197 | return CargoUtils::formatError( wfMessage( "cargo-declare-parenttable-bad-parameter", $extraParamKey )->parse() ); |
198 | } |
199 | } |
200 | } |
201 | } |
202 | } else { |
203 | $parentTableName = trim( $table ); |
204 | } |
205 | if ( $parentTableName ) { |
206 | if ( !$parentTableAlias ) { |
207 | if ( array_key_exists( '_localField', $parentTable ) && |
208 | $parentTable['_localField'] != '_pageName' ) { |
209 | $parentTableAlias = strtolower( $parentTable['_localField'] ); |
210 | if ( array_key_exists( $parentTableAlias, $parentTables ) ) { |
211 | $count = |
212 | substr_count( implode( ',', array_keys( $parentTables ) ), |
213 | $parentTableAlias ); |
214 | $parentTableAlias .= "_$count"; |
215 | } |
216 | } else { |
217 | $parentTableAlias = CargoUtils::makeDifferentAlias( $parentTableName ); |
218 | if ( array_key_exists( $parentTableAlias, $parentTables ) ) { |
219 | $count = |
220 | substr_count( implode( ',', array_keys( $parentTables ) ), |
221 | $parentTableAlias ); |
222 | $parentTableAlias .= "_$count"; |
223 | } |
224 | } |
225 | } |
226 | if ( !array_key_exists( '_localField', $parentTable ) ) { |
227 | $parentTable['_localField'] = '_pageName'; |
228 | } |
229 | if ( !array_key_exists( '_remoteField', $parentTable ) ) { |
230 | $parentTable['_remoteField'] = '_pageName'; |
231 | } |
232 | $parentTable['Name'] = $parentTableName; |
233 | $parentTables[$parentTableAlias] = $parentTable; |
234 | |
235 | // Validate the parent table's name. |
236 | $validationError = self::validateFieldOrTableName( $parentTableName, 'parent table' ); |
237 | if ( $validationError !== null ) { |
238 | return CargoUtils::formatError( $validationError ); |
239 | } |
240 | } |
241 | } |
242 | } elseif ( $key == '_drilldownTabs' ) { |
243 | $value = CargoUtils::smartSplit( ',', $value ); |
244 | foreach ( $value as $tabValues ) { |
245 | $foundMatch = preg_match_all( '/([^(]*)\s*\(?([^)]*)\)?/s', $tabValues, $matches ); |
246 | if ( $foundMatch == false ) { |
247 | continue; |
248 | } |
249 | $tabName = $matches[1][0]; |
250 | $tabName = trim( $tabName ); |
251 | if ( !$tabName ) { |
252 | continue; |
253 | } |
254 | $extraParams = $matches[2][0]; |
255 | $params = preg_split( '~(?<!\\\)' . preg_quote( ';', '~' ) . '~', $extraParams ); |
256 | $tabParams = []; |
257 | foreach ( $params as $param ) { |
258 | if ( !$param ) { |
259 | continue; |
260 | } |
261 | $param = array_map( 'trim', explode( '=', $param, 2 ) ); |
262 | if ( $param[0] == 'fields' ) { |
263 | $fields = array_map( 'trim', explode( ',', $param[1] ) ); |
264 | $drilldownFields = []; |
265 | foreach ( $fields as $field ) { |
266 | $field = array_map( 'trim', explode( '=', $field ) ); |
267 | if ( count( $field ) == 2 ) { |
268 | $drilldownFields[$field[1]] = $field[0]; |
269 | } else { |
270 | $drilldownFields[] = $field[0]; |
271 | } |
272 | } |
273 | $tabParams[$param[0]] = $drilldownFields; |
274 | } else { |
275 | $tabParams[$param[0]] = $param[1]; |
276 | } |
277 | } |
278 | if ( !array_key_exists( 'format', $tabParams ) ) { |
279 | $tabParams['format'] = 'category'; |
280 | } |
281 | if ( !array_key_exists( 'fields', $tabParams ) ) { |
282 | $tabParams['fields'] = [ 'Title' => '_pageName' ]; |
283 | } |
284 | $drilldownTabsParams[$tabName] = $tabParams; |
285 | } |
286 | } else { |
287 | $fieldName = $key; |
288 | $fieldDescriptionStr = $value; |
289 | // Validate field name. |
290 | $validationError = self::validateFieldOrTableName( $fieldName, 'field' ); |
291 | if ( $validationError !== null ) { |
292 | return CargoUtils::formatError( $validationError ); |
293 | } |
294 | |
295 | try { |
296 | $fieldDescription = CargoFieldDescription::newFromString( $fieldDescriptionStr ); |
297 | } catch ( Exception $e ) { |
298 | return CargoUtils::formatError( $e->getMessage() ); |
299 | } |
300 | if ( $fieldDescription == null ) { |
301 | return CargoUtils::formatError( wfMessage( "cargo-declare-field-parse-fail", $fieldName )->parse() ); |
302 | } |
303 | if ( $fieldDescription->mIsHierarchy == true && $fieldDescription->mType == 'Coordinates' ) { |
304 | return CargoUtils::formatError( wfMessage( "cargo-declare-bad-hierarchy-type", $fieldDescription->mType )->parse() ); |
305 | } |
306 | if ( $fieldDescription->mType == "Start date" || $fieldDescription->mType == "Start datetime" ) { |
307 | if ( !$hasStartEvent ) { |
308 | $hasStartEvent = true; |
309 | } else { |
310 | return CargoUtils::formatError( wfMessage( "cargo-declare-ne1-startdttm" )->parse() ); |
311 | } |
312 | } |
313 | if ( $fieldDescription->mType == "End date" || $fieldDescription->mType == "End datetime" ) { |
314 | if ( !$hasEndEvent && $hasStartEvent ) { |
315 | $hasEndEvent = true; |
316 | } elseif ( !$hasStartEvent ) { |
317 | // if End date/datetime is declared before Start date/datetime type field |
318 | return CargoUtils::formatError( wfMessage( "cargo-declare-def-start-before-end" )->parse() ); |
319 | } else { |
320 | return CargoUtils::formatError( wfMessage( "cargo-declare-ne1-enddttm" )->parse() ); |
321 | } |
322 | } |
323 | $tableSchema->mFieldDescriptions[$fieldName] = $fieldDescription; |
324 | } |
325 | } |
326 | |
327 | // Validate table name. |
328 | if ( $tableName == '' ) { |
329 | return CargoUtils::formatError( wfMessage( "cargo-notable" )->parse() ); |
330 | } |
331 | $validationError = self::validateFieldOrTableName( $tableName, 'table' ); |
332 | if ( $validationError !== null ) { |
333 | return CargoUtils::formatError( $validationError ); |
334 | } |
335 | |
336 | // Validate table name. |
337 | |
338 | $cdb = CargoUtils::getDB(); |
339 | |
340 | foreach ( $parentTables as $extraParams ) { |
341 | $parentTableName = $extraParams['Name']; |
342 | $localField = $extraParams['_localField']; |
343 | $remoteField = $extraParams['_remoteField']; |
344 | |
345 | // Validate that parent table exists. |
346 | if ( !$cdb->tableExists( $parentTableName, __METHOD__ ) ) { |
347 | // orig, gives wrong tablename |
348 | return CargoUtils::formatError( wfMessage( "cargo-declare-parenttable-not-exist", $parentTableName )->parse() ); |
349 | } |
350 | |
351 | // Validate that remote field exists. |
352 | if ( !$cdb->fieldExists( $parentTableName, $remoteField, __METHOD__ ) ) { |
353 | return CargoUtils::formatError( wfMessage( "cargo-declare-parenttable-no-field", $parentTableName, $remoteField )->parse() ); |
354 | } |
355 | |
356 | // Validate that local field exists; |
357 | // this needs to be validated against what is declared |
358 | // rather than against the DB, since the table may |
359 | // not be built yet. |
360 | $parentLocalFieldOK = false; |
361 | // Declared field names are stored in CargoTableSchema() |
362 | // object $tableSchema - check declared field names. |
363 | foreach ( $tableSchema->mFieldDescriptions as $a => $b ) { |
364 | if ( $a == $localField ) { |
365 | $parentLocalFieldOK = true; |
366 | } |
367 | } |
368 | // Check implied field name _pageName. |
369 | if ( $localField == "_pageName" ) { |
370 | $parentLocalFieldOK = true; |
371 | } |
372 | if ( !$parentLocalFieldOK ) { |
373 | return CargoUtils::formatError( wfMessage( "cargo-declare-invalid-localfield", $localField )->parse() ); |
374 | } |
375 | } |
376 | |
377 | $parserOutput = $parser->getOutput(); |
378 | |
379 | $parserOutput->setPageProperty( 'CargoTableName', $tableName ); |
380 | $parserOutput->setPageProperty( 'CargoParentTables', serialize( $parentTables ) ); |
381 | $parserOutput->setPageProperty( 'CargoDrilldownTabsParams', serialize( $drilldownTabsParams ) ); |
382 | $parserOutput->setPageProperty( 'CargoFields', $tableSchema->toDBString() ); |
383 | |
384 | // Link to the Special:CargoTables page for this table, if it |
385 | // exists already - otherwise, explain that it needs to be |
386 | // created. |
387 | $text = wfMessage( 'cargo-definestable', $tableName )->text(); |
388 | $cdb = CargoUtils::getDB(); |
389 | if ( $cdb->tableExists( $tableName, __METHOD__ ) ) { |
390 | $ct = SpecialPage::getTitleFor( 'CargoTables' ); |
391 | $pageName = $ct->getPrefixedText() . "/$tableName"; |
392 | $viewTableMsg = wfMessage( 'cargo-cargotables-viewtablelink' )->parse(); |
393 | $text .= " [[$pageName|$viewTableMsg]]."; |
394 | } else { |
395 | $text .= ' ' . wfMessage( 'cargo-tablenotcreated' )->text(); |
396 | } |
397 | |
398 | // Also link to the replacement table, if it exists. |
399 | if ( $cdb->tableExists( $tableName . '__NEXT', __METHOD__ ) ) { |
400 | $text .= ' ' . wfMessage( "cargo-cargotables-replacementgenerated" )->parse(); |
401 | $ctPage = CargoUtils::getSpecialPage( 'CargoTables' ); |
402 | $ctURL = $ctPage->getPageTitle()->getFullURL(); |
403 | $viewURL = $ctURL . '/' . $tableName; |
404 | $viewURL .= strpos( $viewURL, '?' ) ? '&' : '?'; |
405 | $viewURL .= "_replacement"; |
406 | $viewReplacementTableMsg = wfMessage( 'cargo-cargotables-viewreplacementlink' )->parse(); |
407 | $text .= "; [$viewURL $viewReplacementTableMsg]."; |
408 | } |
409 | |
410 | // For use by the Page Exchange extension and possibly others, |
411 | // to automatically generate a Cargo table after the template |
412 | // that declares it is created. |
413 | if ( array_key_exists( 'createData', self::$settings ) ) { |
414 | $userID = self::$settings['userID']; |
415 | $user = MediaWikiServices::getInstance() |
416 | ->getUserFactory() |
417 | ->newFromId( (int)$userID ); |
418 | $title = $parser->getTitle(); |
419 | $templatePageID = $title->getArticleID(); |
420 | CargoUtils::recreateDBTablesForTemplate( |
421 | $templatePageID, |
422 | $createReplacement = false, |
423 | $user, |
424 | $tableName, |
425 | $tableSchema, |
426 | $parentTables |
427 | ); |
428 | |
429 | // Populate (or re-populate) the table with any |
430 | // existing data from the wiki. |
431 | // @todo This should cycle through *all* the pages, |
432 | // 500 at a time, in case there are a lot. |
433 | $titlesToStore = $title->getTemplateLinksTo( [ 'LIMIT' => 1000 ] ); |
434 | $jobs = []; |
435 | foreach ( $titlesToStore as $titleToStore ) { |
436 | $jobs[] = new CargoPopulateTableJob( $titleToStore, [ 'dbTableName' => $tableName ] ); |
437 | } |
438 | MediaWikiServices::getInstance()->getJobQueueGroup()->push( $jobs ); |
439 | |
440 | // Ensure that this code doesn't get called more than |
441 | // once per page save. |
442 | unset( self::$settings['createData'] ); |
443 | } |
444 | |
445 | return $text; |
446 | } |
447 | |
448 | } |