Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.41% |
181 / 217 |
|
63.64% |
7 / 11 |
CRAP | |
0.00% |
0 / 1 |
CargoFieldDescription | |
83.41% |
181 / 217 |
|
63.64% |
7 / 11 |
125.17 | |
0.00% |
0 / 1 |
newFromString | |
88.33% |
53 / 60 |
|
0.00% |
0 / 1 |
20.64 | |||
newFromDBArray | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
14 | |||
getDelimiter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setDelimiter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDateOrDatetime | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFieldSize | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
toDBArray | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
12 | |||
prepareAndValidateValue | |
100.00% |
64 / 64 |
|
100.00% |
1 / 1 |
26 | |||
prettyPrintAllowedValues | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
prettyPrintType | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
prettyPrintTypeAndAttributes | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
56 |
1 | <?php |
2 | /** |
3 | * CargoFieldDescription - holds the attributes of a single field as defined |
4 | * in the #cargo_declare parser function. |
5 | * |
6 | * @author Yaron Koren |
7 | * @ingroup Cargo |
8 | */ |
9 | |
10 | use MediaWiki\Html\Html; |
11 | |
12 | class CargoFieldDescription { |
13 | public $mType; |
14 | public $mSize; |
15 | public $mDependentOn = []; |
16 | public $mIsList = false; |
17 | private $mDelimiter; |
18 | public $mAllowedValues = null; |
19 | public $mIsMandatory = false; |
20 | public $mIsUnique = false; |
21 | public $mRegex = null; |
22 | public $mIsHidden = false; |
23 | public $mIsHierarchy = false; |
24 | public $mHierarchyStructure = null; |
25 | public $mOtherParams = []; |
26 | |
27 | /** |
28 | * Initializes from a string within the #cargo_declare function. |
29 | * |
30 | * @param string $fieldDescriptionStr |
31 | * @return \CargoFieldDescription|null |
32 | */ |
33 | public static function newFromString( $fieldDescriptionStr ) { |
34 | $fieldDescription = new CargoFieldDescription(); |
35 | |
36 | if ( strpos( strtolower( $fieldDescriptionStr ), 'list' ) === 0 ) { |
37 | $matches = []; |
38 | $foundMatch = preg_match( '/[Ll][Ii][Ss][Tt] \((.*)\) [Oo][Ff] (.*)/is', $fieldDescriptionStr, $matches ); |
39 | if ( !$foundMatch ) { |
40 | // Return a true error message here? |
41 | return null; |
42 | } |
43 | $fieldDescription->mIsList = true; |
44 | $fieldDescription->mDelimiter = $matches[1]; |
45 | $fieldDescriptionStr = $matches[2]; |
46 | } |
47 | |
48 | CargoUtils::validateFieldDescriptionString( $fieldDescriptionStr ); |
49 | |
50 | // There may be additional parameters, in/ parentheses. |
51 | $matches = []; |
52 | $foundMatch2 = preg_match( '/([^(]*)\s*\((.*)\)/s', $fieldDescriptionStr, $matches ); |
53 | $allowedValuesParam = ""; |
54 | if ( $foundMatch2 ) { |
55 | $fieldDescriptionStr = trim( $matches[1] ); |
56 | $extraParamsString = $matches[2]; |
57 | $extraParams = explode( ';', $extraParamsString ); |
58 | foreach ( $extraParams as $extraParam ) { |
59 | $extraParamParts = explode( '=', $extraParam, 2 ); |
60 | if ( count( $extraParamParts ) == 1 ) { |
61 | $paramKey = strtolower( trim( $extraParamParts[0] ) ); |
62 | if ( $paramKey == 'hierarchy' ) { |
63 | $fieldDescription->mIsHierarchy = true; |
64 | } |
65 | $fieldDescription->mOtherParams[$paramKey] = true; |
66 | } else { |
67 | $paramKey = strtolower( trim( $extraParamParts[0] ) ); |
68 | $paramValue = trim( $extraParamParts[1] ); |
69 | if ( $paramKey == 'allowed values' ) { |
70 | // we do not assign allowed values to fieldDescription here, |
71 | // because we don't know yet if it's a hierarchy or an enumeration |
72 | $allowedValuesParam = $paramValue; |
73 | } elseif ( $paramKey == 'size' ) { |
74 | $fieldDescription->mSize = $paramValue; |
75 | } elseif ( $paramKey == 'dependent on' ) { |
76 | $fieldDescription->mDependentOn = array_map( 'trim', explode( ',', $paramValue ) ); |
77 | } else { |
78 | $fieldDescription->mOtherParams[$paramKey] = $paramValue; |
79 | } |
80 | } |
81 | } |
82 | if ( $allowedValuesParam !== "" ) { |
83 | $allowedValuesArray = []; |
84 | if ( $fieldDescription->mIsHierarchy == true ) { |
85 | // $paramValue contains "*" hierarchy structure |
86 | CargoUtils::validateHierarchyStructure( trim( $allowedValuesParam ) ); |
87 | $fieldDescription->mHierarchyStructure = trim( $allowedValuesParam ); |
88 | // now make the allowed values param similar to the syntax |
89 | // used by other fields |
90 | $hierarchyNodesArray = explode( "\n", $allowedValuesParam ); |
91 | foreach ( $hierarchyNodesArray as $node ) { |
92 | // Remove prefix of multiple "*" |
93 | $allowedValuesArray[] = trim( preg_replace( '/^[*]*/', '', $node ) ); |
94 | } |
95 | } else { |
96 | // Replace the comma/delimiter |
97 | // substitution with a character |
98 | // that has no chance of being |
99 | // included in the values list - |
100 | // namely, the ASCII beep. |
101 | |
102 | // The delimiter can't be a |
103 | // semicolon, because that's |
104 | // already used to separate |
105 | // "extra parameters", so just |
106 | // hardcode it to a semicolon. |
107 | $delimiter = ','; |
108 | $allowedValuesStr = str_replace( "\\$delimiter", "\a", $allowedValuesParam ); |
109 | $allowedValuesTempArray = explode( $delimiter, $allowedValuesStr ); |
110 | foreach ( $allowedValuesTempArray as $value ) { |
111 | if ( $value == '' ) { |
112 | continue; |
113 | } |
114 | // Replace beep back with delimiter, trim. |
115 | $value = str_replace( "\a", $delimiter, trim( $value ) ); |
116 | $allowedValuesArray[] = $value; |
117 | } |
118 | } |
119 | $fieldDescription->mAllowedValues = $allowedValuesArray; |
120 | } |
121 | } |
122 | |
123 | // What's left will be the type, hopefully. |
124 | // Allow any capitalization of the type. |
125 | $type = ucfirst( strtolower( $fieldDescriptionStr ) ); |
126 | // The 'URL' type has special capitalization. |
127 | if ( $type == 'Url' ) { |
128 | $type = 'URL'; |
129 | } |
130 | $fieldDescription->mType = $type; |
131 | |
132 | // Validation. |
133 | if ( in_array( $type, [ 'Text', 'Wikitext', 'Searchtext' ] ) && |
134 | array_key_exists( 'unique', $fieldDescription->mOtherParams ) ) { |
135 | throw new MWException( "'unique' is not allowed for fields of type '$type'." ); |
136 | } |
137 | if ( $fieldDescription->mType == 'Boolean' && $fieldDescription->mIsList == true ) { |
138 | throw new MWException( "Error: 'list' is not allowed for fields of type 'Boolean'." ); |
139 | } |
140 | |
141 | return $fieldDescription; |
142 | } |
143 | |
144 | /** |
145 | * @param array $descriptionData |
146 | * @return \CargoFieldDescription |
147 | */ |
148 | public static function newFromDBArray( $descriptionData ) { |
149 | $fieldDescription = new CargoFieldDescription(); |
150 | foreach ( $descriptionData as $param => $value ) { |
151 | if ( $param == 'type' ) { |
152 | $fieldDescription->mType = $value; |
153 | } elseif ( $param == 'size' ) { |
154 | $fieldDescription->mSize = $value; |
155 | } elseif ( $param == 'dependent on' ) { |
156 | $fieldDescription->mDependentOn = $value; |
157 | } elseif ( $param == 'isList' ) { |
158 | $fieldDescription->mIsList = true; |
159 | } elseif ( $param == 'delimiter' ) { |
160 | $fieldDescription->mDelimiter = $value; |
161 | } elseif ( $param == 'allowedValues' ) { |
162 | $fieldDescription->mAllowedValues = $value; |
163 | } elseif ( $param == 'mandatory' ) { |
164 | $fieldDescription->mIsMandatory = true; |
165 | } elseif ( $param == 'unique' ) { |
166 | $fieldDescription->mIsUnique = true; |
167 | } elseif ( $param == 'regex' ) { |
168 | $fieldDescription->mRegex = $value; |
169 | } elseif ( $param == 'hidden' ) { |
170 | $fieldDescription->mIsHidden = true; |
171 | } elseif ( $param == 'hierarchy' ) { |
172 | $fieldDescription->mIsHierarchy = true; |
173 | } elseif ( $param == 'hierarchyStructure' ) { |
174 | $fieldDescription->mHierarchyStructure = $value; |
175 | } else { |
176 | $fieldDescription->mOtherParams[$param] = $value; |
177 | } |
178 | } |
179 | return $fieldDescription; |
180 | } |
181 | |
182 | public function getDelimiter() { |
183 | // Make "\n" represent a newline. |
184 | return str_replace( '\n', "\n", $this->mDelimiter ?? '' ); |
185 | } |
186 | |
187 | public function setDelimiter( $delimiter ) { |
188 | $this->mDelimiter = $delimiter; |
189 | } |
190 | |
191 | public function isDateOrDatetime() { |
192 | return in_array( $this->mType, [ 'Date', 'Start date', 'End date', 'Datetime', 'Start datetime', 'End datetime' ] ); |
193 | } |
194 | |
195 | public function getFieldSize() { |
196 | if ( $this->isDateOrDatetime() ) { |
197 | return null; |
198 | } elseif ( in_array( $this->mType, [ 'Integer', 'Float', 'Rating', 'Boolean', 'Text', 'Wikitext', 'Searchtext' ] ) ) { |
199 | return null; |
200 | // This leaves String, Page, etc. - see CargoUtils::fieldTypeToSQLType(). |
201 | } elseif ( $this->mSize != null ) { |
202 | return $this->mSize; |
203 | } else { |
204 | global $wgCargoDefaultStringBytes; |
205 | return $wgCargoDefaultStringBytes; |
206 | } |
207 | } |
208 | |
209 | /** |
210 | * @return array |
211 | */ |
212 | public function toDBArray() { |
213 | $descriptionData = []; |
214 | $descriptionData['type'] = $this->mType; |
215 | if ( $this->mSize != null ) { |
216 | $descriptionData['size'] = $this->mSize; |
217 | } |
218 | if ( $this->mDependentOn != null ) { |
219 | $descriptionData['dependent on'] = $this->mDependentOn; |
220 | } |
221 | if ( $this->mIsList ) { |
222 | $descriptionData['isList'] = true; |
223 | } |
224 | if ( $this->mDelimiter != null ) { |
225 | $descriptionData['delimiter'] = $this->mDelimiter; |
226 | } |
227 | if ( $this->mAllowedValues != null ) { |
228 | $descriptionData['allowedValues'] = $this->mAllowedValues; |
229 | } |
230 | if ( $this->mIsMandatory ) { |
231 | $descriptionData['mandatory'] = true; |
232 | } |
233 | if ( $this->mIsUnique ) { |
234 | $descriptionData['unique'] = true; |
235 | } |
236 | if ( $this->mRegex != null ) { |
237 | $descriptionData['regex'] = $this->mRegex; |
238 | } |
239 | if ( $this->mIsHidden ) { |
240 | $descriptionData['hidden'] = true; |
241 | } |
242 | if ( $this->mIsHierarchy ) { |
243 | $descriptionData['hierarchy'] = true; |
244 | $descriptionData['hierarchyStructure'] = $this->mHierarchyStructure; |
245 | } |
246 | foreach ( $this->mOtherParams as $otherParam => $value ) { |
247 | $descriptionData[$otherParam] = $value; |
248 | } |
249 | |
250 | return $descriptionData; |
251 | } |
252 | |
253 | public function prepareAndValidateValue( $fieldValue ) { |
254 | // @TODO - also set, and return, an error message and/or code |
255 | // if the returned value is different from the incoming value. |
256 | // @TODO - it might make sense to create a new class around |
257 | // this function, like "CargoFieldValue" - |
258 | // CargoStore::getDateValueAndPrecision() could move there too. |
259 | |
260 | // When a `false` (the boolean, not the string 'false') is passed |
261 | // by Lua, `trim( false )` is called. This presumably casts `false` |
262 | // to a string, which in PHP is an empty string. This does not affect |
263 | // `true` however, as `true` casted to a string is '1'. |
264 | // We use '0' if $fieldValue is a boolean false. |
265 | $fieldValue = $fieldValue === false ? '0' : trim( $fieldValue ); |
266 | if ( $fieldValue == '' ) { |
267 | if ( $this->isDateOrDatetime() ) { |
268 | // If it's a date field, it has to be null, |
269 | // not blank, for DB storage to work correctly. |
270 | // Possibly this is true for other types as well. |
271 | return [ 'value' => null ]; |
272 | } |
273 | return [ 'value' => $fieldValue ]; |
274 | } |
275 | |
276 | $fieldType = $this->mType; |
277 | if ( $this->mAllowedValues != null ) { |
278 | $allowedValues = $this->mAllowedValues; |
279 | if ( $this->mIsList ) { |
280 | $delimiter = $this->getDelimiter(); |
281 | $individualValues = explode( $delimiter, $fieldValue ); |
282 | $valuesToBeKept = []; |
283 | foreach ( $individualValues as $individualValue ) { |
284 | $realIndividualVal = trim( $individualValue ); |
285 | if ( in_array( $realIndividualVal, $allowedValues ) ) { |
286 | $valuesToBeKept[] = $realIndividualVal; |
287 | } |
288 | } |
289 | // FIXME: This is dead code, it's overwritten immediately |
290 | $newValue = implode( $delimiter, $valuesToBeKept ); |
291 | } else { |
292 | if ( in_array( $fieldValue, $allowedValues ) ) { |
293 | // FIXME: This is dead code, it's overwritten immediately |
294 | $newValue = $fieldValue; |
295 | } |
296 | } |
297 | } |
298 | |
299 | $precision = null; |
300 | if ( $this->isDateOrDatetime() ) { |
301 | if ( $this->mIsList ) { |
302 | $delimiter = $this->getDelimiter(); |
303 | $individualValues = explode( $delimiter, $fieldValue ); |
304 | // There's unfortunately only one precision |
305 | // value per field, even if it holds more than |
306 | // one date - store the most "precise" of the |
307 | // precision values. |
308 | $maxPrecision = CargoStore::YEAR_ONLY; |
309 | $dateValues = []; |
310 | foreach ( $individualValues as $individualValue ) { |
311 | $realIndividualVal = trim( $individualValue ); |
312 | if ( $realIndividualVal == '' ) { |
313 | continue; |
314 | } |
315 | [ $dateValue, $curPrecision ] = CargoStore::getDateValueAndPrecision( $realIndividualVal, $fieldType ); |
316 | $dateValues[] = $dateValue; |
317 | if ( $curPrecision < $maxPrecision ) { |
318 | $maxPrecision = $curPrecision; |
319 | } |
320 | } |
321 | $newValue = implode( $delimiter, $dateValues ); |
322 | $precision = $maxPrecision; |
323 | } else { |
324 | [ $newValue, $precision ] = CargoStore::getDateValueAndPrecision( $fieldValue, $fieldType ); |
325 | } |
326 | } elseif ( $fieldType == 'Integer' ) { |
327 | // Remove digit-grouping character. |
328 | global $wgCargoDigitGroupingCharacter; |
329 | if ( $this->mIsList ) { |
330 | $delimiter = $this->getDelimiter(); |
331 | if ( $delimiter != $wgCargoDigitGroupingCharacter ) { |
332 | $fieldValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue ); |
333 | } |
334 | $individualValues = explode( $delimiter, $fieldValue ); |
335 | foreach ( $individualValues as &$individualValue ) { |
336 | $individualValue = round( floatval( $individualValue ) ); |
337 | } |
338 | $newValue = implode( $delimiter, $individualValues ); |
339 | } else { |
340 | $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue ); |
341 | $newValue = round( floatval( $newValue ) ); |
342 | } |
343 | } elseif ( $fieldType == 'Float' || $fieldType == 'Rating' ) { |
344 | // Remove digit-grouping character, and change |
345 | // decimal mark to '.' if it's anything else. |
346 | global $wgCargoDigitGroupingCharacter; |
347 | global $wgCargoDecimalMark; |
348 | $newValue = str_replace( $wgCargoDigitGroupingCharacter, '', $fieldValue ); |
349 | $newValue = str_replace( $wgCargoDecimalMark, '.', $newValue ); |
350 | } elseif ( $fieldType == 'Boolean' ) { |
351 | // True = 1, "yes" |
352 | // False = 0, "no" |
353 | $msgForNo = wfMessage( 'htmlform-no' )->text(); |
354 | if ( $fieldValue === 0 |
355 | || $fieldValue === '0' |
356 | || strtolower( $fieldValue ) === 'no' |
357 | || strtolower( $fieldValue ) == strtolower( $msgForNo ) ) { |
358 | $newValue = '0'; |
359 | } else { |
360 | $newValue = '1'; |
361 | } |
362 | } else { |
363 | $newValue = $fieldValue; |
364 | } |
365 | |
366 | $valueArray = [ 'value' => $newValue ]; |
367 | if ( $precision !== null ) { |
368 | $valueArray['precision'] = $precision; |
369 | } |
370 | |
371 | return $valueArray; |
372 | } |
373 | |
374 | public function prettyPrintAllowedValues() { |
375 | $escapedAllowedValues = array_map( 'htmlspecialchars', $this->mAllowedValues ); |
376 | return implode( ' · ', $escapedAllowedValues ); |
377 | } |
378 | |
379 | public function prettyPrintType() { |
380 | $typeDesc = Html::element( 'tt', null, $this->mType ); |
381 | if ( $this->mIsList ) { |
382 | $delimiter = Html::element( 'tt', null, $this->mDelimiter ); |
383 | $typeDesc = wfMessage( 'cargo-cargotables-listof' ) |
384 | ->rawParams( $typeDesc, $delimiter )->escaped(); |
385 | } |
386 | return $typeDesc; |
387 | } |
388 | |
389 | public function prettyPrintTypeAndAttributes() { |
390 | $text = $this->prettyPrintType(); |
391 | |
392 | $attributesStrings = []; |
393 | if ( $this->mIsMandatory ) { |
394 | $attributesStrings[] = [ wfMessage( 'cargo-cargotables-mandatory' )->escaped() ]; |
395 | } |
396 | if ( $this->mIsUnique ) { |
397 | $attributesStrings[] = [ wfMessage( 'cargo-cargotables-unique' )->escaped() ]; |
398 | } |
399 | if ( $this->mAllowedValues !== null ) { |
400 | $allowedValuesStr = $this->prettyPrintAllowedValues(); |
401 | $attributesStrings[] = [ wfMessage( 'cargo-cargotables-allowedvalues' )->escaped(), |
402 | $allowedValuesStr ]; |
403 | } |
404 | if ( count( $attributesStrings ) == 0 ) { |
405 | return $text; |
406 | } |
407 | |
408 | $attributeDisplayStrings = []; |
409 | foreach ( $attributesStrings as $attributesStrs ) { |
410 | $displayString = '<span class="cargoFieldName">' . |
411 | $attributesStrs[0] . '</span>'; |
412 | if ( count( $attributesStrs ) > 1 ) { |
413 | $displayString .= ' ' . $attributesStrs[1]; |
414 | } |
415 | $attributeDisplayStrings[] = $displayString; |
416 | } |
417 | $text .= ' (' . implode( '; ', $attributeDisplayStrings ) . ')'; |
418 | |
419 | return $text; |
420 | } |
421 | |
422 | } |