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